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