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