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