sui_json_rpc/
coin_api.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use 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    // Trait object w/ Box as we do not need to share this across multiple threads
54    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        // exclusive cursor if `Some`, otherwise start from the beginning
125        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                // If cursor is not specified, we need to start from the beginning of the coin type, which is the minimal possible ObjectID.
149                None => (coin_type_tag.to_string(), 0, ObjectID::ZERO),
150            };
151
152            self.internal
153                .get_coins_iterator(
154                    owner, cursor, limit, true, // only care about one type of coin
155                )
156                .await
157        })
158    }
159
160    #[instrument(skip(self))]
161    async fn get_all_coins(
162        &self,
163        owner: SuiAddress,
164        // exclusive cursor if `Some`, otherwise start from the beginning
165        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                    // If cursor is None, start from the beginning
182                    (
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, // return all types of coins
194                )
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                // note: LockedCoin is deprecated
221                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                        // note: LockedCoin is deprecated
241                        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 &currency.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/// CoinReadInternal trait to capture logic of interactions with AuthorityState and metrics
364/// This allows us to also mock internal implementation for testing
365#[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    // Trait object w/ Arc as we have methods that require sharing this across multiple threads
395    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(&currency_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(&currency).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        // Success scenarios
765        #[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            // Build request params
858            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            // Build request params
900            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        // Expected error scenarios
955        #[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        // Unexpected error scenarios
995        #[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        // Success scenarios
1055        #[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        // Expected error scenarios
1136        #[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        // Success scenarios
1188        #[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        // Expected error scenarios
1263        #[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        // Unexpected error scenarios
1284        #[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            // Validate that we handle and return an error message when we encounter an unexpected error
1312            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        // Success scenarios
1342        #[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            // This is because the underlying result is a hashmap, so order is not guaranteed
1394            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        // Unexpected error scenarios
1406        #[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        // Success scenarios
1436        #[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            // Mock object store that returns None for registry lookup
1455            let mut mock_object_store = MockObjectStore::new();
1456            mock_object_store.expect_get_object().return_once(|_| None); // No registry entry
1457
1458            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            // Mock object store that returns None for registry lookup
1495            let mut mock_object_store = MockObjectStore::new();
1496            mock_object_store.expect_get_object().return_once(|_| None); // No registry entry
1497
1498            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            // Mock object store that returns None for registry lookup
1544            let mut mock_object_store = MockObjectStore::new();
1545            mock_object_store.expect_get_object().return_once(|_| None); // No registry entry
1546
1547            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            // return TreasuryCap instead of CoinMetadata to set up test
1557            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            // Derive currency ID the same way the function does
1606            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            // Mock object store that returns None for registry lookup
1664            let mut mock_object_store = MockObjectStore::new();
1665            mock_object_store.expect_get_object().return_once(|_| None); // No registry entry
1666
1667            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            // Mock object store that returns None for registry lookup
1707            let mut mock_object_store = MockObjectStore::new();
1708            mock_object_store.expect_get_object().return_once(|_| None); // No registry entry
1709
1710            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            // Mock object store that returns None for registry lookup
1760            let mut mock_object_store = MockObjectStore::new();
1761            mock_object_store.expect_get_object().return_once(|_| None); // No registry entry
1762
1763            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            // Derive currency ID the same way the function does
1827            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            // Derive currency ID the same way the function does
1879            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}