1use std::collections::HashMap;
5use std::sync::Arc;
6
7use async_trait::async_trait;
8use cached::SizedCache;
9use cached::proc_macro::cached;
10use jsonrpsee::RpcModule;
11use jsonrpsee::core::RpcResult;
12use move_core_types::language_storage::{StructTag, TypeTag};
13use sui_core::jsonrpc_index::TotalBalance;
14use tap::TapFallible;
15use tracing::{debug, instrument};
16
17use sui_core::authority::AuthorityState;
18use sui_json_rpc_api::{CoinReadApiOpenRpc, CoinReadApiServer, JsonRpcMetrics, cap_page_limit};
19use sui_json_rpc_types::Balance;
20use sui_json_rpc_types::{CoinPage, SuiCoinMetadata};
21use sui_open_rpc::Module;
22use sui_storage::key_value_store::TransactionKeyValueStore;
23use sui_types::balance::Supply;
24use sui_types::base_types::{ObjectID, SuiAddress};
25use sui_types::coin::{CoinMetadata, TreasuryCap};
26use sui_types::coin_registry::{Currency, SupplyState};
27use sui_types::effects::TransactionEffectsAPI;
28use sui_types::gas_coin::{GAS, TOTAL_SUPPLY_MIST};
29use sui_types::object::Object;
30use sui_types::parse_sui_struct_tag;
31use sui_types::storage::ObjectStore;
32
33#[cfg(test)]
34use mockall::automock;
35
36use crate::authority_state::StateRead;
37use crate::error::{Error, RpcInterimResult, SuiRpcInputError};
38use crate::{SuiRpcModule, with_tracing};
39
40pub fn parse_to_struct_tag(coin_type: &str) -> Result<StructTag, SuiRpcInputError> {
41 parse_sui_struct_tag(coin_type)
42 .map_err(|e| SuiRpcInputError::CannotParseSuiStructTag(format!("{e}")))
43}
44
45pub fn parse_to_type_tag(coin_type: Option<String>) -> Result<TypeTag, SuiRpcInputError> {
46 Ok(TypeTag::Struct(Box::new(match coin_type {
47 Some(c) => parse_to_struct_tag(&c)?,
48 None => GAS::type_(),
49 })))
50}
51
52pub struct CoinReadApi {
53 internal: Box<dyn CoinReadInternal + Send + Sync>,
55}
56
57impl CoinReadApi {
58 pub fn new(
59 state: Arc<AuthorityState>,
60 transaction_kv_store: Arc<TransactionKeyValueStore>,
61 metrics: Arc<JsonRpcMetrics>,
62 ) -> Self {
63 Self {
64 internal: Box::new(CoinReadInternalImpl::new(
65 state,
66 transaction_kv_store,
67 metrics,
68 )),
69 }
70 }
71}
72
73impl SuiRpcModule for CoinReadApi {
74 fn rpc(self) -> RpcModule<Self> {
75 self.into_rpc()
76 }
77
78 fn rpc_doc_module() -> Module {
79 CoinReadApiOpenRpc::module_doc()
80 }
81}
82
83#[derive(serde::Serialize, serde::Deserialize)]
84struct CoinCursor {
85 coin_type: String,
86 inverted_balance: u64,
87 object_id: ObjectID,
88}
89
90impl CoinCursor {
91 fn new(coin_type: String, balance: u64, object_id: ObjectID) -> Self {
92 Self {
93 coin_type,
94 inverted_balance: !balance,
95 object_id,
96 }
97 }
98
99 fn encode(&self) -> String {
100 use base64::Engine;
101 use base64::prelude::BASE64_STANDARD;
102
103 let json = serde_json::to_string(self).unwrap();
104
105 BASE64_STANDARD.encode(json.as_bytes())
106 }
107
108 fn decode(cursor: &str) -> Option<Self> {
109 use base64::Engine;
110 use base64::prelude::BASE64_STANDARD;
111
112 let bytes = BASE64_STANDARD.decode(cursor).ok()?;
113 serde_json::from_slice(&bytes).ok()
114 }
115}
116
117#[async_trait]
118impl CoinReadApiServer for CoinReadApi {
119 #[instrument(skip(self))]
120 async fn get_coins(
121 &self,
122 owner: SuiAddress,
123 coin_type: Option<String>,
124 cursor: Option<String>,
126 limit: Option<usize>,
127 ) -> RpcResult<CoinPage> {
128 with_tracing!(async move {
129 let coin_type_tag = parse_to_type_tag(coin_type)?;
130
131 let cursor = match cursor {
132 Some(c) => {
133 let decoded = CoinCursor::decode(&c).ok_or_else(|| {
134 SuiRpcInputError::GenericInvalid("invalid cursor".to_string())
135 })?;
136
137 if coin_type_tag.to_string() != decoded.coin_type {
138 return Err(
139 SuiRpcInputError::GenericInvalid("invalid cursor".to_string()).into(),
140 );
141 }
142 (
143 decoded.coin_type,
144 decoded.inverted_balance,
145 decoded.object_id,
146 )
147 }
148 None => (coin_type_tag.to_string(), 0, ObjectID::ZERO),
150 };
151
152 self.internal
153 .get_coins_iterator(
154 owner, cursor, limit, true, )
156 .await
157 })
158 }
159
160 #[instrument(skip(self))]
161 async fn get_all_coins(
162 &self,
163 owner: SuiAddress,
164 cursor: Option<String>,
166 limit: Option<usize>,
167 ) -> RpcResult<CoinPage> {
168 with_tracing!(async move {
169 let cursor = match cursor {
170 Some(c) => {
171 let decoded = CoinCursor::decode(&c).ok_or_else(|| {
172 SuiRpcInputError::GenericInvalid("invalid cursor".to_string())
173 })?;
174 (
175 decoded.coin_type,
176 decoded.inverted_balance,
177 decoded.object_id,
178 )
179 }
180 None => {
181 (
183 String::from_utf8([0u8].to_vec()).unwrap(),
184 0,
185 ObjectID::ZERO,
186 )
187 }
188 };
189
190 let coins = self
191 .internal
192 .get_coins_iterator(
193 owner, cursor, limit, false, )
195 .await?;
196
197 Ok(coins)
198 })
199 }
200
201 #[instrument(skip(self))]
202 async fn get_balance(
203 &self,
204 owner: SuiAddress,
205 coin_type: Option<String>,
206 ) -> RpcResult<Balance> {
207 with_tracing!(async move {
208 let coin_type_tag = parse_to_type_tag(coin_type)?;
209 let balance = self
210 .internal
211 .get_balance(owner, coin_type_tag.clone())
212 .await
213 .tap_err(|e| {
214 debug!(?owner, "Failed to get balance with error: {:?}", e);
215 })?;
216 Ok(Balance {
217 coin_type: coin_type_tag.to_string(),
218 coin_object_count: balance.num_coins as usize,
219 total_balance: balance.balance as u128,
220 locked_balance: Default::default(),
222 })
223 })
224 }
225
226 #[instrument(skip(self))]
227 async fn get_all_balances(&self, owner: SuiAddress) -> RpcResult<Vec<Balance>> {
228 with_tracing!(async move {
229 let all_balance = self.internal.get_all_balance(owner).await.tap_err(|e| {
230 debug!(?owner, "Failed to get all balance with error: {:?}", e);
231 })?;
232 Ok(all_balance
233 .iter()
234 .map(|(coin_type, balance)| {
235 Balance {
236 coin_type: coin_type.to_string(),
237 coin_object_count: balance.num_coins as usize,
238 total_balance: balance.balance as u128,
239 locked_balance: Default::default(),
241 }
242 })
243 .collect())
244 })
245 }
246
247 #[instrument(skip(self))]
248 async fn get_coin_metadata(&self, coin_type: String) -> RpcResult<Option<SuiCoinMetadata>> {
249 with_tracing!(async move {
250 let coin_struct = parse_to_struct_tag(&coin_type)?;
251
252 let state = self.internal.get_state();
253 let object_store = state.get_object_store();
254 if let Some(currency) = get_currency_from_registry(object_store, &coin_struct).await? {
255 return Ok(Some(currency.into()));
256 }
257
258 let metadata_object = self
259 .internal
260 .find_package_object(
261 &coin_struct.address.into(),
262 CoinMetadata::type_(coin_struct),
263 )
264 .await
265 .ok();
266 Ok(metadata_object.and_then(|v: Object| v.try_into().ok()))
267 })
268 }
269
270 #[instrument(skip(self))]
271 async fn get_total_supply(&self, coin_type: String) -> RpcResult<Supply> {
272 with_tracing!(async move {
273 let coin_struct = parse_to_struct_tag(&coin_type)?;
274
275 if GAS::is_gas(&coin_struct) {
276 return Ok(Supply {
277 value: TOTAL_SUPPLY_MIST,
278 });
279 }
280
281 let state = self.internal.get_state();
282 let object_store = state.get_object_store();
283 if let Some(currency) = get_currency_from_registry(object_store, &coin_struct).await? {
284 match ¤cy.supply {
285 Some(SupplyState::Fixed(supply)) | Some(SupplyState::BurnOnly(supply)) => {
286 return Ok(Supply { value: *supply });
287 }
288 _ => {
289 if let Some(treasury_cap_id) = currency.treasury_cap_id {
290 let state = self.internal.get_state();
291 let object_store = state.get_object_store();
292 if let Some(treasury_cap_obj) =
293 object_store.get_object(&treasury_cap_id)
294 {
295 let treasury_cap = TreasuryCap::from_bcs_bytes(
296 treasury_cap_obj
297 .data
298 .try_as_move()
299 .ok_or_else(|| {
300 Error::UnexpectedError(
301 "Treasury cap is not a Move object".to_string(),
302 )
303 })?
304 .contents(),
305 )
306 .map_err(Error::from)?;
307 return Ok(treasury_cap.total_supply);
308 }
309 }
310 }
311 }
312 }
313
314 let treasury_cap_object = self
315 .internal
316 .find_package_object(&coin_struct.address.into(), TreasuryCap::type_(coin_struct))
317 .await?;
318 let treasury_cap = TreasuryCap::from_bcs_bytes(
319 treasury_cap_object.data.try_as_move().unwrap().contents(),
320 )
321 .map_err(Error::from)?;
322 Ok(treasury_cap.total_supply)
323 })
324 }
325}
326
327#[cached(
328 type = "SizedCache<String, ObjectID>",
329 create = "{ SizedCache::with_size(10000) }",
330 convert = r#"{ format!("{}{}", package_id, object_struct_tag) }"#,
331 result = true
332)]
333async fn find_package_object_id(
334 state: Arc<dyn StateRead>,
335 package_id: ObjectID,
336 object_struct_tag: StructTag,
337 kv_store: Arc<TransactionKeyValueStore>,
338) -> RpcInterimResult<ObjectID> {
339 async move {
340 let publish_txn_digest = state.find_publish_txn_digest(package_id)?;
341
342 let effect = kv_store.get_fx_by_tx_digest(publish_txn_digest).await?;
343
344 for ((id, _, _), _) in effect.created() {
345 if let Ok(object_read) = state.get_object_read(&id)
346 && let Ok(object) = object_read.into_object()
347 && matches!(object.type_(), Some(type_) if type_.is(&object_struct_tag))
348 {
349 return Ok(id);
350 }
351 }
352 Err(SuiRpcInputError::GenericNotFound(format!(
353 "Cannot find object with type [{}] from [{}] package created objects.",
354 object_struct_tag, package_id,
355 ))
356 .into())
357 }
358 .await
359}
360
361#[cfg_attr(test, automock)]
364#[async_trait]
365pub trait CoinReadInternal {
366 fn get_state(&self) -> Arc<dyn StateRead>;
367 async fn get_object(&self, object_id: &ObjectID) -> RpcInterimResult<Option<Object>>;
368 async fn get_balance(
369 &self,
370 owner: SuiAddress,
371 coin_type: TypeTag,
372 ) -> RpcInterimResult<TotalBalance>;
373 async fn get_all_balance(
374 &self,
375 owner: SuiAddress,
376 ) -> RpcInterimResult<Arc<HashMap<TypeTag, TotalBalance>>>;
377 async fn find_package_object(
378 &self,
379 package_id: &ObjectID,
380 object_struct_tag: StructTag,
381 ) -> RpcInterimResult<Object>;
382 async fn get_coins_iterator(
383 &self,
384 owner: SuiAddress,
385 cursor: (String, u64, ObjectID),
386 limit: Option<usize>,
387 one_coin_type_only: bool,
388 ) -> RpcInterimResult<CoinPage>;
389}
390
391pub struct CoinReadInternalImpl {
392 state: Arc<dyn StateRead>,
394 transaction_kv_store: Arc<TransactionKeyValueStore>,
395 pub metrics: Arc<JsonRpcMetrics>,
396}
397
398impl CoinReadInternalImpl {
399 pub fn new(
400 state: Arc<AuthorityState>,
401 transaction_kv_store: Arc<TransactionKeyValueStore>,
402 metrics: Arc<JsonRpcMetrics>,
403 ) -> Self {
404 Self {
405 state,
406 transaction_kv_store,
407 metrics,
408 }
409 }
410}
411
412async fn get_currency_from_registry(
413 object_store: &Arc<dyn ObjectStore + Send + Sync>,
414 coin_type: &StructTag,
415) -> RpcInterimResult<Option<Currency>> {
416 let currency_id = Currency::derive_object_id(coin_type.clone().into()).map_err(|e| {
417 Error::UnexpectedError(format!(
418 "Failed to derive Currency ID for coin type {}: {}",
419 coin_type, e
420 ))
421 })?;
422
423 let currency_obj = match object_store.get_object(¤cy_id) {
424 Some(obj) => obj,
425 None => return Ok(None),
426 };
427
428 let move_obj = currency_obj.data.try_as_move().ok_or_else(|| {
429 Error::UnexpectedError(format!(
430 "Currency for coin type {} is not a Move object",
431 coin_type
432 ))
433 })?;
434
435 let currency = bcs::from_bytes::<Currency>(move_obj.contents()).map_err(|e| {
436 Error::UnexpectedError(format!(
437 "Failed to deserialize Currency for coin type {}: {}",
438 coin_type, e
439 ))
440 })?;
441
442 Ok(Some(currency))
443}
444
445#[async_trait]
446impl CoinReadInternal for CoinReadInternalImpl {
447 fn get_state(&self) -> Arc<dyn StateRead> {
448 self.state.clone()
449 }
450
451 async fn get_object(&self, object_id: &ObjectID) -> RpcInterimResult<Option<Object>> {
452 Ok(self.state.get_object(object_id).await?)
453 }
454
455 async fn get_balance(
456 &self,
457 owner: SuiAddress,
458 coin_type: TypeTag,
459 ) -> RpcInterimResult<TotalBalance> {
460 Ok(self.state.get_balance(owner, coin_type).await?)
461 }
462
463 async fn get_all_balance(
464 &self,
465 owner: SuiAddress,
466 ) -> RpcInterimResult<Arc<HashMap<TypeTag, TotalBalance>>> {
467 Ok(self.state.get_all_balance(owner).await?)
468 }
469
470 async fn find_package_object(
471 &self,
472 package_id: &ObjectID,
473 object_struct_tag: StructTag,
474 ) -> RpcInterimResult<Object> {
475 let state = self.get_state();
476 let kv_store = self.transaction_kv_store.clone();
477 let object_id =
478 find_package_object_id(state, *package_id, object_struct_tag, kv_store).await?;
479 Ok(self.state.get_object_read(&object_id)?.into_object()?)
480 }
481
482 async fn get_coins_iterator(
483 &self,
484 owner: SuiAddress,
485 cursor: (String, u64, ObjectID),
486 limit: Option<usize>,
487 one_coin_type_only: bool,
488 ) -> RpcInterimResult<CoinPage> {
489 let limit = cap_page_limit(limit);
490 self.metrics.get_coins_limit.observe(limit as f64);
491 let state = self.get_state();
492 let mut data = state.get_owned_coins(owner, cursor, limit + 1, one_coin_type_only)?;
493
494 let has_next_page = data.len() > limit;
495 data.truncate(limit);
496
497 self.metrics
498 .get_coins_result_size
499 .observe(data.len() as f64);
500 self.metrics
501 .get_coins_result_size_total
502 .inc_by(data.len() as u64);
503 let next_cursor = has_next_page
504 .then(|| {
505 data.last().map(|coin| {
506 CoinCursor::new(coin.coin_type.clone(), coin.balance, coin.coin_object_id)
507 .encode()
508 })
509 })
510 .flatten();
511 Ok(CoinPage {
512 data,
513 next_cursor,
514 has_next_page,
515 })
516 }
517}
518
519#[cfg(test)]
520mod tests {
521 use super::*;
522 use crate::authority_state::{MockStateRead, StateReadError};
523 use expect_test::expect;
524 use mockall::mock;
525 use mockall::predicate;
526 use move_core_types::account_address::AccountAddress;
527 use move_core_types::language_storage::StructTag;
528 use sui_json_rpc_types::Coin;
529 use sui_storage::key_value_store::{
530 KVStoreCheckpointData, KVStoreTransactionData, TransactionKeyValueStoreTrait,
531 };
532 use sui_storage::key_value_store_metrics::KeyValueStoreMetrics;
533 use sui_types::balance::Supply;
534 use sui_types::base_types::{ObjectID, SequenceNumber, SuiAddress};
535 use sui_types::coin::TreasuryCap;
536 use sui_types::digests::{ObjectDigest, TransactionDigest};
537 use sui_types::effects::{TransactionEffects, TransactionEvents};
538 use sui_types::error::{SuiError, SuiErrorKind, SuiResult};
539 use sui_types::gas_coin::GAS;
540 use sui_types::id::UID;
541 use sui_types::messages_checkpoint::{CheckpointDigest, CheckpointSequenceNumber};
542 use sui_types::object::MoveObject;
543 use sui_types::object::Object;
544 use sui_types::object::Owner;
545 use sui_types::utils::create_fake_transaction;
546 use sui_types::{TypeTag, parse_sui_struct_tag};
547
548 mock! {
549 pub KeyValueStore {}
550 #[async_trait]
551 impl TransactionKeyValueStoreTrait for KeyValueStore {
552 async fn multi_get(
553 &self,
554 transactions: &[TransactionDigest],
555 effects: &[TransactionDigest],
556 ) -> SuiResult<KVStoreTransactionData>;
557
558 async fn multi_get_checkpoints(
559 &self,
560 checkpoint_summaries: &[CheckpointSequenceNumber],
561 checkpoint_contents: &[CheckpointSequenceNumber],
562 checkpoint_summaries_by_digest: &[CheckpointDigest],
563 ) -> SuiResult<KVStoreCheckpointData>;
564
565 async fn deprecated_get_transaction_checkpoint(
566 &self,
567 digest: TransactionDigest,
568 ) -> SuiResult<Option<CheckpointSequenceNumber>>;
569
570 async fn get_object(&self, object_id: ObjectID, version: SequenceNumber) -> SuiResult<Option<Object>>;
571 async fn multi_get_objects(&self, object_keys: &[sui_types::storage::ObjectKey]) -> SuiResult<Vec<Option<Object>>>;
572
573 async fn multi_get_transaction_checkpoint(
574 &self,
575 digests: &[TransactionDigest],
576 ) -> SuiResult<Vec<Option<CheckpointSequenceNumber>>>;
577
578 async fn multi_get_events_by_tx_digests(&self,digests: &[TransactionDigest]) -> SuiResult<Vec<Option<TransactionEvents>>>;
579 }
580 }
581
582 impl CoinReadInternalImpl {
583 pub fn new_for_tests(
584 state: Arc<MockStateRead>,
585 kv_store: Option<Arc<MockKeyValueStore>>,
586 ) -> Self {
587 let kv_store = kv_store.unwrap_or_else(|| Arc::new(MockKeyValueStore::new()));
588 let metrics = KeyValueStoreMetrics::new_for_tests();
589 let transaction_kv_store =
590 Arc::new(TransactionKeyValueStore::new("rocksdb", metrics, kv_store));
591 Self {
592 state,
593 transaction_kv_store,
594 metrics: Arc::new(JsonRpcMetrics::new_for_tests()),
595 }
596 }
597 }
598
599 impl CoinReadApi {
600 pub fn new_for_tests(
601 state: Arc<MockStateRead>,
602 kv_store: Option<Arc<MockKeyValueStore>>,
603 ) -> Self {
604 let kv_store = kv_store.unwrap_or_else(|| Arc::new(MockKeyValueStore::new()));
605 Self {
606 internal: Box::new(CoinReadInternalImpl::new_for_tests(state, Some(kv_store))),
607 }
608 }
609 }
610
611 fn get_test_owner() -> SuiAddress {
612 AccountAddress::ONE.into()
613 }
614
615 fn get_test_package_id() -> ObjectID {
616 ObjectID::from_hex_literal("0xf").unwrap()
617 }
618
619 fn get_test_coin_type(package_id: ObjectID) -> String {
620 format!("{}::test_coin::TEST_COIN", package_id)
621 }
622
623 fn get_test_coin_type_tag(coin_type: String) -> TypeTag {
624 TypeTag::Struct(Box::new(parse_sui_struct_tag(&coin_type).unwrap()))
625 }
626
627 enum CoinType {
628 Gas,
629 Usdc,
630 }
631
632 fn get_test_coin(id_hex_literal: Option<&str>, coin_type: CoinType) -> (Object, Coin) {
633 let (arr, coin_type_string, balance, default_hex) = match coin_type {
634 CoinType::Gas => ([0; 32], GAS::type_().to_string(), 42, "0xA"),
635 CoinType::Usdc => (
636 [1; 32],
637 "0x168da5bf1f48dafc111b0a488fa454aca95e0b5e::usdc::USDC".to_string(),
638 24,
639 "0xB",
640 ),
641 };
642
643 let object_id = if let Some(literal) = id_hex_literal {
644 ObjectID::from_hex_literal(literal).unwrap()
645 } else {
646 ObjectID::from_hex_literal(default_hex).unwrap()
647 };
648 let owner = get_test_owner();
649 let previous_transaction = TransactionDigest::from(arr);
650 let object = Object::new_move(
651 MoveObject::new_coin(
652 coin_type_string.parse::<TypeTag>().unwrap(),
653 1.into(),
654 object_id,
655 balance,
656 ),
657 Owner::AddressOwner(owner),
658 previous_transaction,
659 );
660
661 let coin = Coin {
662 coin_type: coin_type_string,
663 coin_object_id: object_id,
664 version: SequenceNumber::from_u64(1),
665 digest: ObjectDigest::from(arr),
666 balance,
667 previous_transaction,
668 };
669
670 (object, coin)
671 }
672
673 fn get_test_treasury_cap_peripherals(
674 package_id: ObjectID,
675 ) -> (String, StructTag, StructTag, TreasuryCap, Object) {
676 let coin_name = get_test_coin_type(package_id);
677 let input_coin_struct = parse_sui_struct_tag(&coin_name).expect("should not fail");
678 let treasury_cap_struct = TreasuryCap::type_(input_coin_struct.clone());
679 let treasury_cap = TreasuryCap {
680 id: UID::new(get_test_package_id()),
681 total_supply: Supply { value: 420 },
682 };
683 let treasury_cap_object =
684 Object::treasury_cap_for_testing(input_coin_struct.clone(), treasury_cap.clone());
685 (
686 coin_name,
687 input_coin_struct,
688 treasury_cap_struct,
689 treasury_cap,
690 treasury_cap_object,
691 )
692 }
693
694 fn create_currency_object(currency: Currency) -> Object {
695 use sui_types::object::{Data, ObjectInner};
696
697 let currency_bytes = bcs::to_bytes(¤cy).unwrap();
698 let data = Data::Move(unsafe {
699 MoveObject::new_from_execution_with_limit(
700 move_core_types::language_storage::StructTag {
701 address: move_core_types::account_address::AccountAddress::from_hex_literal(
702 "0x2",
703 )
704 .unwrap(),
705 module: move_core_types::identifier::Identifier::new("coin_registry").unwrap(),
706 name: move_core_types::identifier::Identifier::new("Currency").unwrap(),
707 type_params: vec![],
708 }
709 .into(),
710 false,
711 SequenceNumber::from_u64(1),
712 currency_bytes,
713 10000,
714 )
715 .unwrap()
716 });
717 ObjectInner {
718 data,
719 owner: Owner::Shared {
720 initial_shared_version: SequenceNumber::from_u64(1),
721 },
722 previous_transaction: TransactionDigest::from([0; 32]),
723 storage_rebate: 0,
724 }
725 .into()
726 }
727
728 mock! {
729 pub ObjectStore {}
730 impl sui_types::storage::ObjectStore for ObjectStore {
731 fn get_object(&self, object_id: &ObjectID) -> Option<Object>;
732 fn get_object_by_key(&self, object_id: &ObjectID, version: SequenceNumber) -> Option<Object>;
733 }
734 }
735
736 fn setup_mock_object_store_with_registry(
737 currency_id: ObjectID,
738 currency_object: Object,
739 treasury_cap: Option<(ObjectID, Object)>,
740 ) -> MockObjectStore {
741 let mut mock_object_store = MockObjectStore::new();
742
743 mock_object_store
744 .expect_get_object()
745 .with(predicate::eq(currency_id))
746 .return_once(move |_| Some(currency_object));
747
748 if let Some((treasury_cap_id, treasury_cap_object)) = treasury_cap {
749 mock_object_store
750 .expect_get_object()
751 .with(predicate::eq(treasury_cap_id))
752 .return_once(move |_| Some(treasury_cap_object));
753 }
754
755 mock_object_store
756 }
757
758 mod get_coins_tests {
759 use super::super::*;
760 use super::*;
761
762 #[tokio::test]
764 async fn test_gas_coin_no_cursor() {
765 let owner = get_test_owner();
766 let gas_coin = get_test_coin(None, CoinType::Gas).1;
767 let gas_coin_clone = gas_coin.clone();
768 let mut mock_state = MockStateRead::new();
769 mock_state
770 .expect_get_owned_coins()
771 .with(
772 predicate::eq(owner),
773 predicate::eq((GAS::type_().to_string(), 0, ObjectID::ZERO)),
774 predicate::eq(51),
775 predicate::eq(true),
776 )
777 .return_once(move |_, _, _, _| Ok(vec![gas_coin_clone]));
778
779 let coin_read_api = CoinReadApi::new_for_tests(Arc::new(mock_state), None);
780 let response = coin_read_api.get_coins(owner, None, None, None).await;
781 assert!(response.is_ok());
782 let result = response.unwrap();
783 assert_eq!(
784 result,
785 CoinPage {
786 data: vec![gas_coin.clone()],
787 next_cursor: None,
788 has_next_page: false,
789 }
790 );
791 }
792
793 #[tokio::test]
794 async fn test_gas_coin_with_cursor() {
795 let owner = get_test_owner();
796 let limit = 2;
797 let coins = vec![
798 get_test_coin(Some("0xA"), CoinType::Gas).1,
799 get_test_coin(Some("0xAA"), CoinType::Gas).1,
800 get_test_coin(Some("0xAAA"), CoinType::Gas).1,
801 ];
802 let coins_clone = coins.clone();
803 let mut mock_state = MockStateRead::new();
804 mock_state
805 .expect_get_owned_coins()
806 .with(
807 predicate::eq(owner),
808 predicate::eq((
809 GAS::type_().to_string(),
810 !coins[0].balance,
811 coins[0].coin_object_id,
812 )),
813 predicate::eq(limit + 1),
814 predicate::eq(true),
815 )
816 .return_once(move |_, _, _, _| Ok(coins_clone));
817 mock_state
818 .expect_get_object()
819 .with(predicate::eq(coins[0].coin_object_id))
820 .return_once(|_| Ok(Some(get_test_coin(Some("0xA"), CoinType::Gas).0)));
821
822 let coin_read_api = CoinReadApi::new_for_tests(Arc::new(mock_state), None);
823 let cursor = CoinCursor::new(
824 coins[0].coin_type.clone(),
825 coins[0].balance,
826 coins[0].coin_object_id,
827 )
828 .encode();
829 let response = coin_read_api
830 .get_coins(owner, None, Some(cursor), Some(limit))
831 .await;
832 assert!(response.is_ok());
833 let result = response.unwrap();
834
835 let expected_cursor = CoinCursor::new(
836 coins[limit - 1].coin_type.clone(),
837 coins[limit - 1].balance,
838 coins[limit - 1].coin_object_id,
839 )
840 .encode();
841 assert_eq!(
842 result,
843 CoinPage {
844 data: coins[..limit].to_vec(),
845 next_cursor: Some(expected_cursor),
846 has_next_page: true,
847 }
848 );
849 }
850
851 #[tokio::test]
852 async fn test_coin_no_cursor() {
853 let coin = get_test_coin(None, CoinType::Usdc).1;
854 let coin_clone = coin.clone();
855 let owner = get_test_owner();
857 let coin_type = coin.coin_type.clone();
858
859 let coin_type_tag =
860 TypeTag::Struct(Box::new(parse_sui_struct_tag(&coin.coin_type).unwrap()));
861 let mut mock_state = MockStateRead::new();
862 mock_state
863 .expect_get_owned_coins()
864 .with(
865 predicate::eq(owner),
866 predicate::eq((coin_type_tag.to_string(), 0, ObjectID::ZERO)),
867 predicate::eq(51),
868 predicate::eq(true),
869 )
870 .return_once(move |_, _, _, _| Ok(vec![coin_clone]));
871
872 let coin_read_api = CoinReadApi::new_for_tests(Arc::new(mock_state), None);
873 let response = coin_read_api
874 .get_coins(owner, Some(coin_type), None, None)
875 .await;
876
877 assert!(response.is_ok());
878 let result = response.unwrap();
879 assert_eq!(
880 result,
881 CoinPage {
882 data: vec![coin.clone()],
883 next_cursor: None,
884 has_next_page: false,
885 }
886 );
887 }
888
889 #[tokio::test]
890 async fn test_coin_with_cursor() {
891 let coins = vec![
892 get_test_coin(Some("0xB"), CoinType::Usdc).1,
893 get_test_coin(Some("0xBB"), CoinType::Usdc).1,
894 get_test_coin(Some("0xBBB"), CoinType::Usdc).1,
895 ];
896 let coins_clone = coins.clone();
897 let owner = get_test_owner();
899 let coin_type = coins[0].coin_type.clone();
900 let cursor = CoinCursor::new(
901 coins[0].coin_type.clone(),
902 coins[0].balance,
903 coins[0].coin_object_id,
904 )
905 .encode();
906 let limit = 2;
907
908 let coin_type_tag =
909 TypeTag::Struct(Box::new(parse_sui_struct_tag(&coins[0].coin_type).unwrap()));
910 let mut mock_state = MockStateRead::new();
911 mock_state
912 .expect_get_owned_coins()
913 .with(
914 predicate::eq(owner),
915 predicate::eq((
916 coin_type_tag.to_string(),
917 !coins[0].balance,
918 coins[0].coin_object_id,
919 )),
920 predicate::eq(limit + 1),
921 predicate::eq(true),
922 )
923 .return_once(move |_, _, _, _| Ok(coins_clone));
924 mock_state
925 .expect_get_object()
926 .with(predicate::eq(coins[0].coin_object_id))
927 .return_once(|_| Ok(Some(get_test_coin(Some("0xBB"), CoinType::Usdc).0)));
928
929 let coin_read_api = CoinReadApi::new_for_tests(Arc::new(mock_state), None);
930 let response = coin_read_api
931 .get_coins(owner, Some(coin_type), Some(cursor), Some(limit))
932 .await;
933
934 assert!(response.is_ok());
935 let result = response.unwrap();
936 let expected_cursor = CoinCursor::new(
937 coins[limit - 1].coin_type.clone(),
938 coins[limit - 1].balance,
939 coins[limit - 1].coin_object_id,
940 )
941 .encode();
942 assert_eq!(
943 result,
944 CoinPage {
945 data: coins[..limit].to_vec(),
946 next_cursor: Some(expected_cursor),
947 has_next_page: true,
948 }
949 );
950 }
951
952 #[tokio::test]
954 async fn test_invalid_coin_type() {
955 let owner = get_test_owner();
956 let coin_type = "0x2::invalid::struct::tag";
957 let mock_state = MockStateRead::new();
958 let coin_read_api = CoinReadApi::new_for_tests(Arc::new(mock_state), None);
959 let response = coin_read_api
960 .get_coins(owner, Some(coin_type.to_string()), None, None)
961 .await;
962
963 assert!(response.is_err());
964 let error_object = response.unwrap_err();
965 let expected = expect!["-32602"];
966 expected.assert_eq(&error_object.code().to_string());
967 let expected = expect![
968 "Invalid struct type: 0x2::invalid::struct::tag. Got error: Expected end of token stream. Got: ::"
969 ];
970 expected.assert_eq(error_object.message());
971 }
972
973 #[tokio::test]
974 async fn test_unrecognized_token() {
975 let owner = get_test_owner();
976 let coin_type = "0x2::sui:🤵";
977 let mock_state = MockStateRead::new();
978 let coin_read_api = CoinReadApi::new_for_tests(Arc::new(mock_state), None);
979 let response = coin_read_api
980 .get_coins(owner, Some(coin_type.to_string()), None, None)
981 .await;
982
983 assert!(response.is_err());
984 let error_object = response.unwrap_err();
985 let expected = expect!["-32602"];
986 expected.assert_eq(&error_object.code().to_string());
987 let expected =
988 expect!["Invalid struct type: 0x2::sui:🤵. Got error: unrecognized token: :🤵"];
989 expected.assert_eq(error_object.message());
990 }
991
992 #[tokio::test]
994 async fn test_get_coins_iterator_index_store_not_available() {
995 let owner = get_test_owner();
996 let coin_type = get_test_coin_type(get_test_package_id());
997 let mut mock_state = MockStateRead::new();
998 mock_state
999 .expect_get_owned_coins()
1000 .returning(move |_, _, _, _| {
1001 Err(StateReadError::Client(
1002 SuiErrorKind::IndexStoreNotAvailable.into(),
1003 ))
1004 });
1005 let coin_read_api = CoinReadApi::new_for_tests(Arc::new(mock_state), None);
1006 let response = coin_read_api
1007 .get_coins(owner, Some(coin_type.to_string()), None, None)
1008 .await;
1009
1010 assert!(response.is_err());
1011 let error_object = response.unwrap_err();
1012 assert_eq!(
1013 error_object.code(),
1014 jsonrpsee::types::error::INVALID_PARAMS_CODE
1015 );
1016 let expected = expect!["Index store not available on this Fullnode."];
1017 expected.assert_eq(error_object.message());
1018 }
1019
1020 #[tokio::test]
1021 async fn test_get_coins_iterator_typed_store_error() {
1022 let owner = get_test_owner();
1023 let coin_type = get_test_coin_type(get_test_package_id());
1024 let mut mock_state = MockStateRead::new();
1025 mock_state
1026 .expect_get_owned_coins()
1027 .returning(move |_, _, _, _| {
1028 Err(SuiErrorKind::Storage("mock rocksdb error".to_string()).into())
1029 });
1030 let coin_read_api = CoinReadApi::new_for_tests(Arc::new(mock_state), None);
1031 let response = coin_read_api
1032 .get_coins(owner, Some(coin_type.to_string()), None, None)
1033 .await;
1034
1035 assert!(response.is_err());
1036 let error_object = response.unwrap_err();
1037 assert_eq!(
1038 error_object.code(),
1039 jsonrpsee::types::error::INTERNAL_ERROR_CODE
1040 );
1041 let expected = expect!["Storage error: mock rocksdb error"];
1042 expected.assert_eq(error_object.message());
1043 }
1044 }
1045
1046 mod get_all_coins_tests {
1047 use sui_types::object::{MoveObject, Owner};
1048
1049 use super::super::*;
1050 use super::*;
1051
1052 #[tokio::test]
1054 async fn test_no_cursor() {
1055 let owner = get_test_owner();
1056 let gas_coin = get_test_coin(None, CoinType::Gas).1;
1057 let gas_coin_clone = gas_coin.clone();
1058 let mut mock_state = MockStateRead::new();
1059 mock_state
1060 .expect_get_owned_coins()
1061 .with(
1062 predicate::eq(owner),
1063 predicate::eq((
1064 String::from_utf8([0u8].to_vec()).unwrap(),
1065 0,
1066 ObjectID::ZERO,
1067 )),
1068 predicate::eq(51),
1069 predicate::eq(false),
1070 )
1071 .return_once(move |_, _, _, _| Ok(vec![gas_coin_clone]));
1072 let coin_read_api = CoinReadApi::new_for_tests(Arc::new(mock_state), None);
1073 let response = coin_read_api
1074 .get_all_coins(owner, None, Some(51))
1075 .await
1076 .unwrap();
1077 assert_eq!(response.data.len(), 1);
1078 assert_eq!(response.data[0], gas_coin);
1079 }
1080
1081 #[tokio::test]
1082 async fn test_with_cursor() {
1083 let owner = get_test_owner();
1084 let limit = 2;
1085 let coins = vec![
1086 get_test_coin(Some("0xA"), CoinType::Gas).1,
1087 get_test_coin(Some("0xAA"), CoinType::Gas).1,
1088 get_test_coin(Some("0xAAA"), CoinType::Gas).1,
1089 ];
1090 let coins_clone = coins.clone();
1091 let coin_move_object = MoveObject::new_gas_coin(
1092 coins[0].version,
1093 coins[0].coin_object_id,
1094 coins[0].balance,
1095 );
1096 let coin_object = Object::new_move(
1097 coin_move_object,
1098 Owner::Immutable,
1099 coins[0].previous_transaction,
1100 );
1101 let mut mock_state = MockStateRead::new();
1102 mock_state
1103 .expect_get_object()
1104 .return_once(move |_| Ok(Some(coin_object)));
1105 mock_state
1106 .expect_get_owned_coins()
1107 .with(
1108 predicate::eq(owner),
1109 predicate::eq((
1110 coins[0].coin_type.clone(),
1111 !coins[0].balance,
1112 coins[0].coin_object_id,
1113 )),
1114 predicate::eq(limit + 1),
1115 predicate::eq(false),
1116 )
1117 .return_once(move |_, _, _, _| Ok(coins_clone));
1118 let coin_read_api = CoinReadApi::new_for_tests(Arc::new(mock_state), None);
1119 let cursor = CoinCursor::new(
1120 coins[0].coin_type.clone(),
1121 coins[0].balance,
1122 coins[0].coin_object_id,
1123 )
1124 .encode();
1125 let response = coin_read_api
1126 .get_all_coins(owner, Some(cursor), Some(limit))
1127 .await
1128 .unwrap();
1129 assert_eq!(response.data.len(), limit);
1130 assert_eq!(response.data, coins[..limit].to_vec());
1131 }
1132
1133 #[tokio::test]
1135 async fn test_object_is_not_coin() {
1136 let owner = get_test_owner();
1137 let object_id = get_test_package_id();
1138 let (_, _, _, _, treasury_cap_object) = get_test_treasury_cap_peripherals(object_id);
1139 let mut mock_state = MockStateRead::new();
1140 mock_state.expect_get_object().returning(move |obj_id| {
1141 if obj_id == &object_id {
1142 Ok(Some(treasury_cap_object.clone()))
1143 } else {
1144 panic!("should not be called with any other object id")
1145 }
1146 });
1147 let coin_read_api = CoinReadApi::new_for_tests(Arc::new(mock_state), None);
1148 let response = coin_read_api
1149 .get_all_coins(owner, Some(object_id.to_string()), None)
1150 .await;
1151
1152 assert!(response.is_err());
1153 let error_object = response.unwrap_err();
1154 assert_eq!(error_object.code(), -32602);
1155 let expected = expect!["-32602"];
1156 expected.assert_eq(&error_object.code().to_string());
1157 let expected = expect!["invalid cursor"];
1158 expected.assert_eq(error_object.message());
1159 }
1160
1161 #[tokio::test]
1162 async fn test_object_not_found() {
1163 let owner = get_test_owner();
1164 let object_id = get_test_package_id();
1165 let mut mock_state = MockStateRead::new();
1166 mock_state.expect_get_object().returning(move |_| Ok(None));
1167
1168 let coin_read_api = CoinReadApi::new_for_tests(Arc::new(mock_state), None);
1169 let response = coin_read_api
1170 .get_all_coins(owner, Some(object_id.to_string()), None)
1171 .await;
1172
1173 assert!(response.is_err());
1174 let error_object = response.unwrap_err();
1175 let expected = expect!["-32602"];
1176 expected.assert_eq(&error_object.code().to_string());
1177 let expected = expect!["invalid cursor"];
1178 expected.assert_eq(error_object.message());
1179 }
1180 }
1181
1182 mod get_balance_tests {
1183 use super::super::*;
1184 use super::*;
1185 #[tokio::test]
1187 async fn test_gas_coin() {
1188 let owner = get_test_owner();
1189 let gas_coin = get_test_coin(None, CoinType::Gas).1;
1190 let gas_coin_clone = gas_coin.clone();
1191 let mut mock_state = MockStateRead::new();
1192 mock_state
1193 .expect_get_balance()
1194 .with(
1195 predicate::eq(owner),
1196 predicate::eq(get_test_coin_type_tag(gas_coin_clone.coin_type)),
1197 )
1198 .return_once(move |_, _| {
1199 Ok(TotalBalance {
1200 balance: 7,
1201 num_coins: 9,
1202 })
1203 });
1204 let coin_read_api = CoinReadApi::new_for_tests(Arc::new(mock_state), None);
1205 let response = coin_read_api.get_balance(owner, None).await;
1206
1207 assert!(response.is_ok());
1208 let result = response.unwrap();
1209 assert_eq!(
1210 result,
1211 Balance {
1212 coin_type: gas_coin.coin_type,
1213 coin_object_count: 9,
1214 total_balance: 7,
1215 locked_balance: Default::default()
1216 }
1217 );
1218 }
1219
1220 #[tokio::test]
1221 async fn test_with_coin_type() {
1222 let owner = get_test_owner();
1223 let coin = get_test_coin(None, CoinType::Usdc).1;
1224 let coin_clone = coin.clone();
1225 let mut mock_state = MockStateRead::new();
1226 mock_state
1227 .expect_get_balance()
1228 .with(
1229 predicate::eq(owner),
1230 predicate::eq(get_test_coin_type_tag(coin_clone.coin_type)),
1231 )
1232 .return_once(move |_, _| {
1233 Ok(TotalBalance {
1234 balance: 10,
1235 num_coins: 11,
1236 })
1237 });
1238 let coin_read_api = CoinReadApi::new_for_tests(Arc::new(mock_state), None);
1239 let response = coin_read_api
1240 .get_balance(owner, Some(coin.coin_type.clone()))
1241 .await;
1242
1243 assert!(response.is_ok());
1244 let result = response.unwrap();
1245 assert_eq!(
1246 result,
1247 Balance {
1248 coin_type: coin.coin_type,
1249 coin_object_count: 11,
1250 total_balance: 10,
1251 locked_balance: Default::default()
1252 }
1253 );
1254 }
1255
1256 #[tokio::test]
1258 async fn test_invalid_coin_type() {
1259 let owner = get_test_owner();
1260 let coin_type = "0x2::invalid::struct::tag";
1261 let mock_state = MockStateRead::new();
1262 let coin_read_api = CoinReadApi::new_for_tests(Arc::new(mock_state), None);
1263 let response = coin_read_api
1264 .get_balance(owner, Some(coin_type.to_string()))
1265 .await;
1266
1267 assert!(response.is_err());
1268 let error_object = response.unwrap_err();
1269 let expected = expect!["-32602"];
1270 expected.assert_eq(&error_object.code().to_string());
1271 let expected = expect![
1272 "Invalid struct type: 0x2::invalid::struct::tag. Got error: Expected end of token stream. Got: ::"
1273 ];
1274 expected.assert_eq(error_object.message());
1275 }
1276
1277 #[tokio::test]
1279 async fn test_get_balance_index_store_not_available() {
1280 let owner = get_test_owner();
1281 let coin_type = get_test_coin_type(get_test_package_id());
1282 let mut mock_state = MockStateRead::new();
1283 mock_state.expect_get_balance().returning(move |_, _| {
1284 Err(StateReadError::Client(
1285 SuiErrorKind::IndexStoreNotAvailable.into(),
1286 ))
1287 });
1288 let coin_read_api = CoinReadApi::new_for_tests(Arc::new(mock_state), None);
1289 let response = coin_read_api
1290 .get_balance(owner, Some(coin_type.to_string()))
1291 .await;
1292
1293 assert!(response.is_err());
1294 let error_object = response.unwrap_err();
1295 assert_eq!(
1296 error_object.code(),
1297 jsonrpsee::types::error::INVALID_PARAMS_CODE
1298 );
1299 let expected = expect!["Index store not available on this Fullnode."];
1300 expected.assert_eq(error_object.message());
1301 }
1302
1303 #[tokio::test]
1304 async fn test_get_balance_execution_error() {
1305 let owner = get_test_owner();
1307 let coin_type = get_test_coin_type(get_test_package_id());
1308 let mut mock_state = MockStateRead::new();
1309 mock_state.expect_get_balance().returning(move |_, _| {
1310 Err(SuiErrorKind::ExecutionError("mock db error".to_string()).into())
1311 });
1312 let coin_read_api = CoinReadApi::new_for_tests(Arc::new(mock_state), None);
1313 let response = coin_read_api
1314 .get_balance(owner, Some(coin_type.to_string()))
1315 .await;
1316
1317 assert!(response.is_err());
1318 let error_object = response.unwrap_err();
1319
1320 assert_eq!(
1321 error_object.code(),
1322 jsonrpsee::types::error::INTERNAL_ERROR_CODE
1323 );
1324 let expected = expect!["Error executing mock db error"];
1325 expected.assert_eq(error_object.message());
1326 }
1327 }
1328
1329 mod get_all_balances_tests {
1330 use sui_types::error::SuiErrorKind;
1331
1332 use super::super::*;
1333 use super::*;
1334
1335 #[tokio::test]
1337 async fn test_success_scenario() {
1338 let owner = get_test_owner();
1339 let gas_coin = get_test_coin(None, CoinType::Gas).1;
1340 let gas_coin_type_tag = get_test_coin_type_tag(gas_coin.coin_type.clone());
1341 let usdc_coin = get_test_coin(None, CoinType::Usdc).1;
1342 let usdc_coin_type_tag = get_test_coin_type_tag(usdc_coin.coin_type.clone());
1343 let mut mock_state = MockStateRead::new();
1344 mock_state
1345 .expect_get_all_balance()
1346 .with(predicate::eq(owner))
1347 .return_once(move |_| {
1348 let mut hash_map = HashMap::new();
1349 hash_map.insert(
1350 gas_coin_type_tag,
1351 TotalBalance {
1352 balance: 7,
1353 num_coins: 9,
1354 },
1355 );
1356 hash_map.insert(
1357 usdc_coin_type_tag,
1358 TotalBalance {
1359 balance: 10,
1360 num_coins: 11,
1361 },
1362 );
1363 Ok(Arc::new(hash_map))
1364 });
1365 let coin_read_api = CoinReadApi::new_for_tests(Arc::new(mock_state), None);
1366 let response = coin_read_api.get_all_balances(owner).await;
1367
1368 assert!(response.is_ok());
1369 let expected_result = vec![
1370 Balance {
1371 coin_type: gas_coin.coin_type,
1372 coin_object_count: 9,
1373 total_balance: 7,
1374 locked_balance: Default::default(),
1375 },
1376 Balance {
1377 coin_type: usdc_coin.coin_type,
1378 coin_object_count: 11,
1379 total_balance: 10,
1380 locked_balance: Default::default(),
1381 },
1382 ];
1383 let mut result = response.unwrap();
1385 for item in expected_result {
1386 if let Some(pos) = result.iter().position(|i| *i == item) {
1387 result.remove(pos);
1388 } else {
1389 panic!("{:?} not found in result", item);
1390 }
1391 }
1392 assert!(result.is_empty());
1393 }
1394
1395 #[tokio::test]
1397 async fn test_index_store_not_available() {
1398 let owner = get_test_owner();
1399 let mut mock_state = MockStateRead::new();
1400 mock_state.expect_get_all_balance().returning(move |_| {
1401 Err(StateReadError::Client(
1402 SuiError(Box::new(SuiErrorKind::IndexStoreNotAvailable)).into(),
1403 ))
1404 });
1405 let coin_read_api = CoinReadApi::new_for_tests(Arc::new(mock_state), None);
1406 let response = coin_read_api.get_all_balances(owner).await;
1407
1408 assert!(response.is_err());
1409 let error_object = response.unwrap_err();
1410 assert_eq!(
1411 error_object.code(),
1412 jsonrpsee::types::error::INVALID_PARAMS_CODE
1413 );
1414 let expected = expect!["Index store not available on this Fullnode."];
1415 expected.assert_eq(error_object.message());
1416 }
1417 }
1418
1419 mod get_coin_metadata_tests {
1420 use super::super::*;
1421 use super::*;
1422 use mockall::predicate;
1423 use sui_types::id::UID;
1424
1425 #[tokio::test]
1427 async fn test_valid_coin_metadata_object() {
1428 let package_id = get_test_package_id();
1429 let coin_name = get_test_coin_type(package_id);
1430 let input_coin_struct = parse_sui_struct_tag(&coin_name).expect("should not fail");
1431 let coin_metadata_struct = CoinMetadata::type_(input_coin_struct.clone());
1432 let coin_metadata = CoinMetadata {
1433 id: UID::new(get_test_package_id()),
1434 decimals: 2,
1435 name: "test_coin".to_string(),
1436 symbol: "TEST".to_string(),
1437 description: "test coin".to_string(),
1438 icon_url: Some("unit.test.io".to_string()),
1439 };
1440 let coin_metadata_object =
1441 Object::coin_metadata_for_testing(input_coin_struct.clone(), coin_metadata);
1442 let metadata = SuiCoinMetadata::try_from(coin_metadata_object.clone()).unwrap();
1443
1444 let mut mock_object_store = MockObjectStore::new();
1446 mock_object_store.expect_get_object().return_once(|_| None); let mut mock_state = MockStateRead::new();
1449 mock_state
1450 .expect_get_object_store()
1451 .return_const(Arc::new(mock_object_store) as Arc<dyn ObjectStore + Send + Sync>);
1452
1453 let mut mock_internal = MockCoinReadInternal::new();
1454 mock_internal
1455 .expect_get_state()
1456 .return_once(move || Arc::new(mock_state) as Arc<dyn StateRead>);
1457 mock_internal
1458 .expect_find_package_object()
1459 .with(predicate::always(), predicate::eq(coin_metadata_struct))
1460 .return_once(move |object_id, _| {
1461 if object_id == &package_id {
1462 Ok(coin_metadata_object)
1463 } else {
1464 panic!("should not be called with any other object id")
1465 }
1466 });
1467
1468 let coin_read_api = CoinReadApi {
1469 internal: Box::new(mock_internal),
1470 };
1471
1472 let response = coin_read_api.get_coin_metadata(coin_name.clone()).await;
1473 assert!(response.is_ok());
1474 let result = response.unwrap().unwrap();
1475 assert_eq!(result, metadata);
1476 }
1477
1478 #[tokio::test]
1479 async fn test_object_not_found() {
1480 let transaction_digest = TransactionDigest::from([0; 32]);
1481 let transaction_effects = TransactionEffects::default();
1482 let transaction_effects_clone = transaction_effects.clone();
1483
1484 let mut mock_object_store = MockObjectStore::new();
1486 mock_object_store.expect_get_object().return_once(|_| None); let mut mock_state = MockStateRead::new();
1489 mock_state
1490 .expect_get_object_store()
1491 .return_const(Arc::new(mock_object_store) as Arc<dyn ObjectStore + Send + Sync>);
1492 mock_state
1493 .expect_find_publish_txn_digest()
1494 .return_once(move |_| Ok(transaction_digest));
1495 mock_state
1496 .expect_get_executed_transaction_and_effects()
1497 .return_once(move |_, _| {
1498 Ok((create_fake_transaction(), transaction_effects.clone()))
1499 });
1500
1501 let mut mock_kv_store = MockKeyValueStore::new();
1502 mock_kv_store.expect_multi_get().return_once(move |_, _| {
1503 Ok((
1504 vec![Some(create_fake_transaction())],
1505 vec![Some(transaction_effects_clone)],
1506 ))
1507 });
1508
1509 let coin_read_api =
1510 CoinReadApi::new_for_tests(Arc::new(mock_state), Some(Arc::new(mock_kv_store)));
1511 let response = coin_read_api
1512 .get_coin_metadata("0x2::sui::SUI".to_string())
1513 .await;
1514
1515 assert!(response.is_ok());
1516 let result = response.unwrap();
1517 assert_eq!(result, None);
1518 }
1519
1520 #[tokio::test]
1521 async fn test_find_package_object_not_sui_coin_metadata() {
1522 let package_id = get_test_package_id();
1523 let coin_name = get_test_coin_type(package_id);
1524 let input_coin_struct = parse_sui_struct_tag(&coin_name).expect("should not fail");
1525 let coin_metadata_struct = CoinMetadata::type_(input_coin_struct.clone());
1526 let treasury_cap = TreasuryCap {
1527 id: UID::new(get_test_package_id()),
1528 total_supply: Supply { value: 420 },
1529 };
1530 let treasury_cap_object =
1531 Object::treasury_cap_for_testing(input_coin_struct.clone(), treasury_cap);
1532
1533 let mut mock_object_store = MockObjectStore::new();
1535 mock_object_store.expect_get_object().return_once(|_| None); let mut mock_state = MockStateRead::new();
1538 mock_state
1539 .expect_get_object_store()
1540 .return_const(Arc::new(mock_object_store) as Arc<dyn ObjectStore + Send + Sync>);
1541
1542 let mut mock_internal = MockCoinReadInternal::new();
1543 mock_internal
1544 .expect_get_state()
1545 .return_once(move || Arc::new(mock_state) as Arc<dyn StateRead>);
1546 mock_internal
1548 .expect_find_package_object()
1549 .with(predicate::always(), predicate::eq(coin_metadata_struct))
1550 .returning(move |object_id, _| {
1551 if object_id == &package_id {
1552 Ok(treasury_cap_object.clone())
1553 } else {
1554 panic!("should not be called with any other object id")
1555 }
1556 });
1557
1558 let coin_read_api = CoinReadApi {
1559 internal: Box::new(mock_internal),
1560 };
1561
1562 let response = coin_read_api.get_coin_metadata(coin_name.clone()).await;
1563 assert!(response.is_ok());
1564 let result = response.unwrap();
1565 assert!(result.is_none());
1566 }
1567
1568 #[tokio::test]
1569 async fn test_coin_metadata_with_registry_data() {
1570 use sui_types::coin_registry::{self, Currency, MetadataCapState, SupplyState};
1571 use sui_types::collection_types::VecMap;
1572
1573 let package_id = get_test_package_id();
1574 let coin_name = get_test_coin_type(package_id);
1575 let coin_struct = parse_sui_struct_tag(&coin_name).expect("should not fail");
1576
1577 let currency_object_id = ObjectID::from_hex_literal(
1578 "0xDADA000000000000000000000000000000000000000000000000000000000000",
1579 )
1580 .unwrap();
1581 let currency = Currency {
1582 id: currency_object_id,
1583 decimals: 9,
1584 name: "Registry Test Coin".to_string(),
1585 symbol: "RTC".to_string(),
1586 description: "A coin from the registry".to_string(),
1587 icon_url: "https://registry.test/icon.png".to_string(),
1588 supply: Some(SupplyState::Fixed(1000000)),
1589 regulated: coin_registry::RegulatedState::Unknown,
1590 treasury_cap_id: None,
1591 metadata_cap_id: MetadataCapState::Unclaimed,
1592 extra_fields: VecMap { contents: vec![] },
1593 };
1594
1595 let currency_id = Currency::derive_object_id(coin_struct.clone().into()).unwrap();
1597 let currency_object = create_currency_object(currency);
1598
1599 let mock_object_store =
1600 setup_mock_object_store_with_registry(currency_id, currency_object, None);
1601
1602 let mut mock_state = MockStateRead::new();
1603 mock_state
1604 .expect_get_object_store()
1605 .return_const(Arc::new(mock_object_store) as Arc<dyn ObjectStore + Send + Sync>);
1606
1607 let coin_read_api = CoinReadApi::new_for_tests(Arc::new(mock_state), None);
1608
1609 let response = coin_read_api.get_coin_metadata(coin_name).await;
1610 assert!(response.is_ok());
1611 let result = response.unwrap();
1612 assert!(result.is_some());
1613
1614 let metadata = result.unwrap();
1615 assert_eq!(metadata.decimals, 9);
1616 assert_eq!(metadata.name, "Registry Test Coin");
1617 assert_eq!(metadata.symbol, "RTC");
1618 assert_eq!(metadata.description, "A coin from the registry");
1619 assert_eq!(
1620 metadata.icon_url,
1621 Some("https://registry.test/icon.png".to_string())
1622 );
1623 }
1624 }
1625
1626 mod get_total_supply_tests {
1627 use super::super::*;
1628 use super::*;
1629 use mockall::predicate;
1630 use sui_types::id::UID;
1631
1632 #[tokio::test]
1633 async fn test_success_response_for_gas_coin() {
1634 let coin_type = "0x2::sui::SUI";
1635 let mock_internal = MockCoinReadInternal::new();
1636 let coin_read_api = CoinReadApi {
1637 internal: Box::new(mock_internal),
1638 };
1639
1640 let response = coin_read_api.get_total_supply(coin_type.to_string()).await;
1641
1642 let supply = response.unwrap();
1643 let expected = expect!["10000000000000000000"];
1644 expected.assert_eq(&supply.value.to_string());
1645 }
1646
1647 #[tokio::test]
1648 async fn test_success_response_for_other_coin() {
1649 let package_id = get_test_package_id();
1650 let (coin_name, _, treasury_cap_struct, _, treasury_cap_object) =
1651 get_test_treasury_cap_peripherals(package_id);
1652
1653 let mut mock_object_store = MockObjectStore::new();
1655 mock_object_store.expect_get_object().return_once(|_| None); let mut mock_state = MockStateRead::new();
1658 mock_state
1659 .expect_get_object_store()
1660 .return_const(Arc::new(mock_object_store) as Arc<dyn ObjectStore + Send + Sync>);
1661
1662 let mut mock_internal = MockCoinReadInternal::new();
1663 mock_internal
1664 .expect_get_state()
1665 .return_once(move || Arc::new(mock_state) as Arc<dyn StateRead>);
1666 mock_internal
1667 .expect_find_package_object()
1668 .with(predicate::always(), predicate::eq(treasury_cap_struct))
1669 .returning(move |object_id, _| {
1670 if object_id == &package_id {
1671 Ok(treasury_cap_object.clone())
1672 } else {
1673 panic!("should not be called with any other object id")
1674 }
1675 });
1676 let coin_read_api = CoinReadApi {
1677 internal: Box::new(mock_internal),
1678 };
1679
1680 let response = coin_read_api.get_total_supply(coin_name.clone()).await;
1681
1682 assert!(response.is_ok());
1683 let result = response.unwrap();
1684 let expected = expect!["420"];
1685 expected.assert_eq(&result.value.to_string());
1686 }
1687
1688 #[tokio::test]
1689 async fn test_object_not_found() {
1690 let package_id = get_test_package_id();
1691 let (coin_name, _, _, _, _) = get_test_treasury_cap_peripherals(package_id);
1692 let transaction_digest = TransactionDigest::from([0; 32]);
1693 let transaction_effects = TransactionEffects::default();
1694 let transaction_effects_clone = transaction_effects.clone();
1695
1696 let mut mock_object_store = MockObjectStore::new();
1698 mock_object_store.expect_get_object().return_once(|_| None); let mut mock_state = MockStateRead::new();
1701 mock_state
1702 .expect_get_object_store()
1703 .return_const(Arc::new(mock_object_store) as Arc<dyn ObjectStore + Send + Sync>);
1704 mock_state
1705 .expect_find_publish_txn_digest()
1706 .return_once(move |_| Ok(transaction_digest));
1707 mock_state
1708 .expect_multi_get()
1709 .return_once(move |_, _| Ok((vec![], vec![Some(transaction_effects.clone())])));
1710
1711 let mut mock_kv_store = MockKeyValueStore::new();
1712 mock_kv_store.expect_multi_get().return_once(move |_, _| {
1713 Ok((
1714 vec![Some(create_fake_transaction())],
1715 vec![Some(transaction_effects_clone)],
1716 ))
1717 });
1718
1719 let coin_read_api =
1720 CoinReadApi::new_for_tests(Arc::new(mock_state), Some(Arc::new(mock_kv_store)));
1721 let response = coin_read_api.get_total_supply(coin_name.clone()).await;
1722
1723 assert!(response.is_err());
1724 let error_object = response.unwrap_err();
1725 let expected = expect!["-32602"];
1726 expected.assert_eq(&error_object.code().to_string());
1727 let expected = expect![
1728 "Cannot find object with type [0x2::coin::TreasuryCap<0xf::test_coin::TEST_COIN>] from [0x000000000000000000000000000000000000000000000000000000000000000f] package created objects."
1729 ];
1730 expected.assert_eq(error_object.message());
1731 }
1732
1733 #[tokio::test]
1734 async fn test_find_package_object_not_treasury_cap() {
1735 let package_id = get_test_package_id();
1736 let (coin_name, input_coin_struct, treasury_cap_struct, _, _) =
1737 get_test_treasury_cap_peripherals(package_id);
1738 let coin_metadata = CoinMetadata {
1739 id: UID::new(get_test_package_id()),
1740 decimals: 2,
1741 name: "test_coin".to_string(),
1742 symbol: "TEST".to_string(),
1743 description: "test coin".to_string(),
1744 icon_url: None,
1745 };
1746 let coin_metadata_object =
1747 Object::coin_metadata_for_testing(input_coin_struct.clone(), coin_metadata);
1748
1749 let mut mock_object_store = MockObjectStore::new();
1751 mock_object_store.expect_get_object().return_once(|_| None); let mut mock_state = MockStateRead::new();
1754 mock_state
1755 .expect_get_object_store()
1756 .return_const(Arc::new(mock_object_store) as Arc<dyn ObjectStore + Send + Sync>);
1757
1758 let mut mock_internal = MockCoinReadInternal::new();
1759 mock_internal
1760 .expect_get_state()
1761 .return_once(move || Arc::new(mock_state) as Arc<dyn StateRead>);
1762 mock_internal
1763 .expect_find_package_object()
1764 .with(predicate::always(), predicate::eq(treasury_cap_struct))
1765 .returning(move |object_id, _| {
1766 if object_id == &package_id {
1767 Ok(coin_metadata_object.clone())
1768 } else {
1769 panic!("should not be called with any other object id")
1770 }
1771 });
1772
1773 let coin_read_api = CoinReadApi {
1774 internal: Box::new(mock_internal),
1775 };
1776
1777 let response = coin_read_api.get_total_supply(coin_name.clone()).await;
1778 let error_object = response.unwrap_err();
1779 assert_eq!(
1780 error_object.code(),
1781 jsonrpsee::types::error::CALL_EXECUTION_FAILED_CODE
1782 );
1783 let expected = expect![
1784 "Failure deserializing object in the requested format: Unable to deserialize TreasuryCap object: remaining input"
1785 ];
1786 expected.assert_eq(error_object.message());
1787 }
1788
1789 #[tokio::test]
1790 async fn test_total_supply_with_registry_fixed_supply() {
1791 use sui_types::coin_registry::{self, Currency, MetadataCapState, SupplyState};
1792 use sui_types::collection_types::VecMap;
1793
1794 let package_id = get_test_package_id();
1795 let coin_name = get_test_coin_type(package_id);
1796 let coin_struct = parse_sui_struct_tag(&coin_name).expect("should not fail");
1797
1798 let currency_uid = ObjectID::from_hex_literal(
1799 "0xDADA200000000000000000000000000000000000000000000000000000000000",
1800 )
1801 .unwrap();
1802 let currency = Currency {
1803 id: currency_uid,
1804 decimals: 18,
1805 name: "Fixed Supply Coin".to_string(),
1806 symbol: "FSC".to_string(),
1807 description: "A coin with fixed supply".to_string(),
1808 icon_url: "https://fixed.supply/icon.png".to_string(),
1809 supply: Some(SupplyState::Fixed(21_000_000_000_000_000)),
1810 regulated: coin_registry::RegulatedState::Unknown,
1811 treasury_cap_id: None,
1812 metadata_cap_id: MetadataCapState::Unclaimed,
1813 extra_fields: VecMap { contents: vec![] },
1814 };
1815
1816 let currency_id = Currency::derive_object_id(coin_struct.clone().into()).unwrap();
1818 let currency_object = create_currency_object(currency);
1819
1820 let mock_object_store =
1821 setup_mock_object_store_with_registry(currency_id, currency_object, None);
1822
1823 let mut mock_state = MockStateRead::new();
1824 mock_state
1825 .expect_get_object_store()
1826 .return_const(Arc::new(mock_object_store) as Arc<dyn ObjectStore + Send + Sync>);
1827
1828 let coin_read_api = CoinReadApi::new_for_tests(Arc::new(mock_state), None);
1829
1830 let response = coin_read_api.get_total_supply(coin_name).await;
1831 assert!(response.is_ok());
1832 let supply = response.unwrap();
1833 assert_eq!(supply.value, 21_000_000_000_000_000);
1834 }
1835
1836 #[tokio::test]
1837 async fn test_total_supply_with_registry_unknown_supply() {
1838 use sui_types::coin_registry::{self, Currency, MetadataCapState, SupplyState};
1839 use sui_types::collection_types::VecMap;
1840
1841 let package_id = get_test_package_id();
1842 let coin_name = get_test_coin_type(package_id);
1843 let coin_struct = parse_sui_struct_tag(&coin_name).expect("should not fail");
1844
1845 let treasury_cap_id = ObjectID::from_hex_literal(
1846 "0x7EA5000000000000000000000000000000000000000000000000000000000000",
1847 )
1848 .unwrap();
1849
1850 let currency_uid = ObjectID::from_hex_literal(
1851 "0xDADA300000000000000000000000000000000000000000000000000000000000",
1852 )
1853 .unwrap();
1854 let currency = Currency {
1855 id: currency_uid,
1856 decimals: 6,
1857 name: "Unknown Supply Coin".to_string(),
1858 symbol: "USC".to_string(),
1859 description: "A coin with unknown supply".to_string(),
1860 icon_url: "https://unknown.supply/icon.png".to_string(),
1861 supply: Some(SupplyState::Unknown),
1862 regulated: coin_registry::RegulatedState::Unknown,
1863 treasury_cap_id: Some(treasury_cap_id),
1864 metadata_cap_id: MetadataCapState::Unclaimed,
1865 extra_fields: VecMap { contents: vec![] },
1866 };
1867
1868 let currency_id = Currency::derive_object_id(coin_struct.clone().into()).unwrap();
1870 let currency_object = create_currency_object(currency);
1871
1872 let treasury_cap = TreasuryCap {
1873 id: UID::new(treasury_cap_id),
1874 total_supply: Supply { value: 100_000_000 },
1875 };
1876 let treasury_cap_object =
1877 Object::treasury_cap_for_testing(coin_struct.clone(), treasury_cap);
1878
1879 let mock_object_store = setup_mock_object_store_with_registry(
1880 currency_id,
1881 currency_object,
1882 Some((treasury_cap_id, treasury_cap_object)),
1883 );
1884
1885 let mut mock_state = MockStateRead::new();
1886 mock_state
1887 .expect_get_object_store()
1888 .return_const(Arc::new(mock_object_store) as Arc<dyn ObjectStore + Send + Sync>);
1889
1890 let coin_read_api = CoinReadApi::new_for_tests(Arc::new(mock_state), None);
1891
1892 let response = coin_read_api.get_total_supply(coin_name).await;
1893 assert!(response.is_ok());
1894 let supply = response.unwrap();
1895 assert_eq!(supply.value, 100_000_000);
1896 }
1897 }
1898}