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