iota_json_rpc/
coin_api.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use std::{collections::HashMap, sync::Arc};
6
7use anyhow::Result;
8use async_trait::async_trait;
9use cached::{SizedCache, proc_macro::cached};
10use chrono::DateTime;
11use iota_core::authority::AuthorityState;
12use iota_json_rpc_api::{CoinReadApiOpenRpc, CoinReadApiServer, JsonRpcMetrics, cap_page_limit};
13use iota_json_rpc_types::{Balance, CoinPage, IotaCirculatingSupply, IotaCoinMetadata};
14use iota_mainnet_unlocks::MainnetUnlocksStore;
15use iota_metrics::spawn_monitored_task;
16use iota_open_rpc::Module;
17use iota_protocol_config::Chain;
18use iota_storage::{indexes::TotalBalance, key_value_store::TransactionKeyValueStore};
19use iota_types::{
20    balance::Supply,
21    base_types::{IotaAddress, ObjectID},
22    coin::{CoinMetadata, TreasuryCap},
23    effects::TransactionEffectsAPI,
24    gas_coin::GAS,
25    iota_system_state::{
26        IotaSystemStateTrait, iota_system_state_summary::IotaSystemStateSummaryV2,
27    },
28    object::Object,
29    parse_iota_struct_tag,
30};
31use jsonrpsee::{RpcModule, core::RpcResult};
32#[cfg(test)]
33use mockall::automock;
34use move_core_types::language_storage::{StructTag, TypeTag};
35use tap::TapFallible;
36use tracing::{debug, instrument};
37
38use crate::{
39    IotaRpcModule,
40    authority_state::StateRead,
41    error::{Error, IotaRpcInputError, RpcInterimResult},
42    logger::FutureWithTracing as _,
43};
44
45pub fn parse_to_struct_tag(coin_type: &str) -> Result<StructTag, IotaRpcInputError> {
46    parse_iota_struct_tag(coin_type)
47        .map_err(|e| IotaRpcInputError::CannotParseIotaStructTag(format!("{e}")))
48}
49
50pub fn parse_to_type_tag(coin_type: Option<String>) -> Result<TypeTag, IotaRpcInputError> {
51    Ok(TypeTag::Struct(Box::new(match coin_type {
52        Some(c) => parse_to_struct_tag(&c)?,
53        None => GAS::type_(),
54    })))
55}
56
57pub struct CoinReadApi {
58    // Trait object w/ Box as we do not need to share this across multiple threads
59    internal: Box<dyn CoinReadInternal + Send + Sync>,
60    unlocks_store: MainnetUnlocksStore,
61}
62
63impl CoinReadApi {
64    pub fn new(
65        state: Arc<AuthorityState>,
66        transaction_kv_store: Arc<TransactionKeyValueStore>,
67        metrics: Arc<JsonRpcMetrics>,
68    ) -> Result<Self> {
69        Ok(Self {
70            internal: Box::new(CoinReadInternalImpl::new(
71                state,
72                transaction_kv_store,
73                metrics,
74            )),
75            unlocks_store: MainnetUnlocksStore::new()?,
76        })
77    }
78}
79
80impl IotaRpcModule for CoinReadApi {
81    fn rpc(self) -> RpcModule<Self> {
82        self.into_rpc()
83    }
84
85    fn rpc_doc_module() -> Module {
86        CoinReadApiOpenRpc::module_doc()
87    }
88}
89
90#[async_trait]
91impl CoinReadApiServer for CoinReadApi {
92    #[instrument(skip(self))]
93    async fn get_coins(
94        &self,
95        owner: IotaAddress,
96        coin_type: Option<String>,
97        // exclusive cursor if `Some`, otherwise start from the beginning
98        cursor: Option<ObjectID>,
99        limit: Option<usize>,
100    ) -> RpcResult<CoinPage> {
101        async move {
102            let coin_type_tag = parse_to_type_tag(coin_type)?;
103
104            let cursor = match cursor {
105                Some(c) => (coin_type_tag.to_string(), c),
106                // If cursor is not specified, we need to start from the beginning of the coin
107                // type, which is the minimal possible ObjectID.
108                None => (coin_type_tag.to_string(), ObjectID::ZERO),
109            };
110
111            self.internal
112                .get_coins_iterator(
113                    owner, cursor, limit, true, // only care about one type of coin
114                )
115                .await
116        }
117        .trace()
118        .await
119    }
120
121    #[instrument(skip(self))]
122    async fn get_all_coins(
123        &self,
124        owner: IotaAddress,
125        // exclusive cursor if `Some`, otherwise start from the beginning
126        cursor: Option<ObjectID>,
127        limit: Option<usize>,
128    ) -> RpcResult<CoinPage> {
129        async move {
130            let cursor = match cursor {
131                Some(object_id) => {
132                    let obj = self.internal.get_object(&object_id).await?;
133                    match obj {
134                        Some(obj) => {
135                            let coin_type = obj.coin_type_maybe();
136                            if coin_type.is_none() {
137                                Err(IotaRpcInputError::GenericInvalid(
138                                    "cursor is not a coin".to_string(),
139                                ))
140                            } else {
141                                Ok((coin_type.unwrap().to_string(), object_id))
142                            }
143                        }
144                        None => Err(IotaRpcInputError::GenericInvalid(
145                            "cursor not found".to_string(),
146                        )),
147                    }
148                }
149                None => {
150                    // If cursor is None, start from the beginning
151                    Ok((String::from_utf8([0u8].to_vec()).unwrap(), ObjectID::ZERO))
152                }
153            }?;
154
155            let coins = self
156                .internal
157                .get_coins_iterator(
158                    owner, cursor, limit, false, // return all types of coins
159                )
160                .await?;
161
162            Ok(coins)
163        }
164        .trace()
165        .await
166    }
167
168    #[instrument(skip(self))]
169    async fn get_balance(
170        &self,
171        owner: IotaAddress,
172        coin_type: Option<String>,
173    ) -> RpcResult<Balance> {
174        async move {
175            let coin_type_tag = parse_to_type_tag(coin_type)?;
176            let balance = self
177                .internal
178                .get_balance(owner, coin_type_tag.clone())
179                .await
180                .tap_err(|e| {
181                    debug!(?owner, "Failed to get balance with error: {:?}", e);
182                })?;
183            Ok(Balance {
184                coin_type: coin_type_tag.to_string(),
185                coin_object_count: balance.num_coins as usize,
186                total_balance: balance.balance as u128,
187            })
188        }
189        .trace()
190        .await
191    }
192
193    #[instrument(skip(self))]
194    async fn get_all_balances(&self, owner: IotaAddress) -> RpcResult<Vec<Balance>> {
195        async move {
196            let all_balance = self.internal.get_all_balance(owner).await.tap_err(|e| {
197                debug!(?owner, "Failed to get all balance with error: {:?}", e);
198            })?;
199            Ok(all_balance
200                .iter()
201                .map(|(coin_type, balance)| Balance {
202                    coin_type: coin_type.to_string(),
203                    coin_object_count: balance.num_coins as usize,
204                    total_balance: balance.balance as u128,
205                })
206                .collect())
207        }
208        .trace()
209        .await
210    }
211
212    #[instrument(skip(self))]
213    async fn get_coin_metadata(&self, coin_type: String) -> RpcResult<Option<IotaCoinMetadata>> {
214        async move {
215            let coin_struct = parse_to_struct_tag(&coin_type)?;
216            let metadata_object = self
217                .internal
218                .find_package_object(
219                    &coin_struct.address.into(),
220                    CoinMetadata::type_(coin_struct),
221                )
222                .await
223                .ok();
224            Ok(metadata_object.and_then(|v: Object| v.try_into().ok()))
225        }
226        .trace()
227        .await
228    }
229
230    #[instrument(skip(self))]
231    async fn get_total_supply(&self, coin_type: String) -> RpcResult<Supply> {
232        async move {
233            let coin_struct = parse_to_struct_tag(&coin_type)?;
234            Ok(if GAS::is_gas(&coin_struct) {
235                let system_state_summary = IotaSystemStateSummaryV2::try_from(
236                    self.internal
237                        .get_state()
238                        .get_system_state()?
239                        .into_iota_system_state_summary(),
240                )?;
241                Supply {
242                    value: system_state_summary.iota_total_supply,
243                }
244            } else {
245                let treasury_cap_object = self
246                    .internal
247                    .find_package_object(
248                        &coin_struct.address.into(),
249                        TreasuryCap::type_(coin_struct),
250                    )
251                    .await?;
252                let treasury_cap = TreasuryCap::from_bcs_bytes(
253                    treasury_cap_object.data.try_as_move().unwrap().contents(),
254                )
255                .map_err(Error::from)?;
256                treasury_cap.total_supply
257            })
258        }
259        .trace()
260        .await
261    }
262
263    #[instrument(skip(self))]
264    async fn get_circulating_supply(&self) -> RpcResult<IotaCirculatingSupply> {
265        let latest_cp_num = self
266            .internal
267            .get_state()
268            .get_latest_checkpoint_sequence_number()
269            .map_err(Error::from)?;
270        let latest_cp = self
271            .internal
272            .get_state()
273            .get_checkpoint_by_sequence_number(latest_cp_num)
274            .map_err(Error::from)?
275            .ok_or(Error::Unexpected("latest checkpoint not found".to_string()))?;
276        let cp_timestamp = latest_cp.timestamp_ms;
277
278        let system_state_summary = IotaSystemStateSummaryV2::try_from(
279            self.internal
280                .get_state()
281                .get_system_state()
282                .map_err(Error::from)?
283                .into_iota_system_state_summary(),
284        )
285        .map_err(Error::from)?;
286
287        let total_supply = system_state_summary.iota_total_supply;
288
289        let date_time = DateTime::from_timestamp_millis(
290            cp_timestamp
291                .try_into()
292                .map_err(|e| Error::Internal(anyhow::Error::from(e)))?,
293        )
294        .ok_or(Error::Unexpected(format!(
295            "failed to parse timestamp: {cp_timestamp}"
296        )))?;
297
298        let chain_identifier = self.internal.get_state().get_chain_identifier();
299        let chain = chain_identifier
300            .map(|c| c.chain())
301            .unwrap_or(Chain::Unknown);
302
303        let locked_supply = match chain {
304            Chain::Mainnet => self.unlocks_store.still_locked_tokens(date_time),
305            _ => 0,
306        };
307
308        let circulating_supply = total_supply - locked_supply;
309        let circulating_supply_percentage = circulating_supply as f64 / total_supply as f64;
310
311        Ok(IotaCirculatingSupply {
312            value: circulating_supply,
313            circulating_supply_percentage,
314            at_checkpoint: *latest_cp.sequence_number(),
315        })
316    }
317}
318
319#[cached(
320    type = "SizedCache<String, ObjectID>",
321    create = "{ SizedCache::with_size(10000) }",
322    convert = r#"{ format!("{}{}", package_id, object_struct_tag) }"#,
323    result = true
324)]
325async fn find_package_object_id(
326    state: Arc<dyn StateRead>,
327    package_id: ObjectID,
328    object_struct_tag: StructTag,
329    kv_store: Arc<TransactionKeyValueStore>,
330) -> RpcInterimResult<ObjectID> {
331    spawn_monitored_task!(async move {
332        let publish_txn_digest = state.find_publish_txn_digest(package_id)?;
333
334        let (_, effect) = state
335            .get_executed_transaction_and_effects(publish_txn_digest, kv_store)
336            .await?;
337
338        for ((id, _, _), _) in effect.created() {
339            if let Ok(object_read) = state.get_object_read(&id) {
340                if let Ok(object) = object_read.into_object() {
341                    if matches!(object.type_(), Some(type_) if type_.is(&object_struct_tag)) {
342                        return Ok(id);
343                    }
344                }
345            }
346        }
347        Err(IotaRpcInputError::GenericNotFound(format!(
348            "Cannot find object [{}] from [{}] package event.",
349            object_struct_tag, package_id,
350        ))
351        .into())
352    })
353    .await?
354}
355
356/// CoinReadInternal trait to capture logic of interactions with AuthorityState
357/// and metrics This allows us to also mock internal implementation for testing
358#[cfg_attr(test, automock)]
359#[async_trait]
360pub trait CoinReadInternal {
361    fn get_state(&self) -> Arc<dyn StateRead>;
362    async fn get_object(&self, object_id: &ObjectID) -> RpcInterimResult<Option<Object>>;
363    async fn get_balance(
364        &self,
365        owner: IotaAddress,
366        coin_type: TypeTag,
367    ) -> RpcInterimResult<TotalBalance>;
368    async fn get_all_balance(
369        &self,
370        owner: IotaAddress,
371    ) -> RpcInterimResult<Arc<HashMap<TypeTag, TotalBalance>>>;
372    async fn find_package_object(
373        &self,
374        package_id: &ObjectID,
375        object_struct_tag: StructTag,
376    ) -> RpcInterimResult<Object>;
377    async fn get_coins_iterator(
378        &self,
379        owner: IotaAddress,
380        cursor: (String, ObjectID),
381        limit: Option<usize>,
382        one_coin_type_only: bool,
383    ) -> RpcInterimResult<CoinPage>;
384}
385
386pub struct CoinReadInternalImpl {
387    // Trait object w/ Arc as we have methods that require sharing this across multiple threads
388    state: Arc<dyn StateRead>,
389    transaction_kv_store: Arc<TransactionKeyValueStore>,
390    pub metrics: Arc<JsonRpcMetrics>,
391}
392
393impl CoinReadInternalImpl {
394    pub fn new(
395        state: Arc<AuthorityState>,
396        transaction_kv_store: Arc<TransactionKeyValueStore>,
397        metrics: Arc<JsonRpcMetrics>,
398    ) -> Self {
399        Self {
400            state,
401            transaction_kv_store,
402            metrics,
403        }
404    }
405}
406
407#[async_trait]
408impl CoinReadInternal for CoinReadInternalImpl {
409    fn get_state(&self) -> Arc<dyn StateRead> {
410        self.state.clone()
411    }
412
413    async fn get_object(&self, object_id: &ObjectID) -> RpcInterimResult<Option<Object>> {
414        Ok(self.state.get_object(object_id).await?)
415    }
416
417    async fn get_balance(
418        &self,
419        owner: IotaAddress,
420        coin_type: TypeTag,
421    ) -> RpcInterimResult<TotalBalance> {
422        Ok(self.state.get_balance(owner, coin_type).await?)
423    }
424
425    async fn get_all_balance(
426        &self,
427        owner: IotaAddress,
428    ) -> RpcInterimResult<Arc<HashMap<TypeTag, TotalBalance>>> {
429        Ok(self.state.get_all_balance(owner).await?)
430    }
431
432    async fn find_package_object(
433        &self,
434        package_id: &ObjectID,
435        object_struct_tag: StructTag,
436    ) -> RpcInterimResult<Object> {
437        let state = self.get_state();
438        let kv_store = self.transaction_kv_store.clone();
439        let object_id =
440            find_package_object_id(state, *package_id, object_struct_tag, kv_store).await?;
441        Ok(self.state.get_object_read(&object_id)?.into_object()?)
442    }
443
444    async fn get_coins_iterator(
445        &self,
446        owner: IotaAddress,
447        cursor: (String, ObjectID),
448        limit: Option<usize>,
449        one_coin_type_only: bool,
450    ) -> RpcInterimResult<CoinPage> {
451        let limit = cap_page_limit(limit);
452        self.metrics.get_coins_limit.observe(limit as f64);
453        let state = self.get_state();
454        let mut data = spawn_monitored_task!(async move {
455            state.get_owned_coins(owner, cursor, limit + 1, one_coin_type_only)
456        })
457        .await??;
458
459        let has_next_page = data.len() > limit;
460        data.truncate(limit);
461
462        self.metrics
463            .get_coins_result_size
464            .observe(data.len() as f64);
465        self.metrics
466            .get_coins_result_size_total
467            .inc_by(data.len() as u64);
468        let next_cursor = data.last().map(|coin| coin.coin_object_id);
469        Ok(CoinPage {
470            data,
471            next_cursor,
472            has_next_page,
473        })
474    }
475}
476
477#[cfg(test)]
478mod tests {
479    use expect_test::expect;
480    use iota_json_rpc_types::Coin;
481    use iota_storage::{
482        key_value_store::{
483            KVStoreCheckpointData, KVStoreTransactionData, TransactionKeyValueStoreTrait,
484        },
485        key_value_store_metrics::KeyValueStoreMetrics,
486    };
487    use iota_types::{
488        TypeTag,
489        balance::Supply,
490        base_types::{IotaAddress, ObjectID, SequenceNumber},
491        coin::TreasuryCap,
492        digests::{ObjectDigest, TransactionDigest},
493        effects::{TransactionEffects, TransactionEvents},
494        error::{IotaError, IotaResult},
495        gas_coin::GAS,
496        id::UID,
497        messages_checkpoint::{CheckpointDigest, CheckpointSequenceNumber},
498        object::Object,
499        parse_iota_struct_tag,
500        utils::create_fake_transaction,
501    };
502    use mockall::{mock, predicate};
503    use move_core_types::{account_address::AccountAddress, language_storage::StructTag};
504
505    use super::*;
506    use crate::authority_state::{MockStateRead, StateReadError};
507
508    mock! {
509        pub KeyValueStore {}
510        #[async_trait]
511        impl TransactionKeyValueStoreTrait for KeyValueStore {
512            async fn multi_get(
513                &self,
514                transaction_keys: &[TransactionDigest],
515                effects_keys: &[TransactionDigest],
516            ) -> IotaResult<KVStoreTransactionData>;
517
518            async fn multi_get_checkpoints(
519                &self,
520                checkpoint_summaries: &[CheckpointSequenceNumber],
521                checkpoint_contents: &[CheckpointSequenceNumber],
522                checkpoint_summaries_by_digest: &[CheckpointDigest],
523            ) -> IotaResult<KVStoreCheckpointData>;
524
525            async fn get_transaction_perpetual_checkpoint(
526                &self,
527                digest: TransactionDigest,
528            ) -> IotaResult<Option<CheckpointSequenceNumber>>;
529
530            async fn get_object(&self, object_id: ObjectID, version: SequenceNumber) -> IotaResult<Option<Object>>;
531
532            async fn multi_get_transactions_perpetual_checkpoints(
533                &self,
534                digests: &[TransactionDigest],
535            ) -> IotaResult<Vec<Option<CheckpointSequenceNumber>>>;
536
537            async fn multi_get_events_by_tx_digests(
538                &self,
539                digests: &[TransactionDigest]
540            ) -> IotaResult<Vec<Option<TransactionEvents>>>;
541        }
542    }
543
544    impl CoinReadInternalImpl {
545        pub fn new_for_tests(
546            state: Arc<MockStateRead>,
547            kv_store: Option<Arc<MockKeyValueStore>>,
548        ) -> Self {
549            let kv_store = kv_store.unwrap_or_else(|| Arc::new(MockKeyValueStore::new()));
550            let metrics = KeyValueStoreMetrics::new_for_tests();
551            let transaction_kv_store =
552                Arc::new(TransactionKeyValueStore::new("rocksdb", metrics, kv_store));
553            Self {
554                state,
555                transaction_kv_store,
556                metrics: Arc::new(JsonRpcMetrics::new_for_tests()),
557            }
558        }
559    }
560
561    impl CoinReadApi {
562        pub fn new_for_tests(
563            state: Arc<MockStateRead>,
564            kv_store: Option<Arc<MockKeyValueStore>>,
565        ) -> Self {
566            let kv_store = kv_store.unwrap_or_else(|| Arc::new(MockKeyValueStore::new()));
567            Self {
568                internal: Box::new(CoinReadInternalImpl::new_for_tests(state, Some(kv_store))),
569                unlocks_store: MainnetUnlocksStore::new().unwrap(),
570            }
571        }
572    }
573
574    fn get_test_owner() -> IotaAddress {
575        AccountAddress::ONE.into()
576    }
577
578    fn get_test_package_id() -> ObjectID {
579        ObjectID::from_hex_literal("0xf").unwrap()
580    }
581
582    fn get_test_coin_type(package_id: ObjectID) -> String {
583        format!("{}::test_coin::TEST_COIN", package_id)
584    }
585
586    fn get_test_coin_type_tag(coin_type: String) -> TypeTag {
587        TypeTag::Struct(Box::new(parse_iota_struct_tag(&coin_type).unwrap()))
588    }
589
590    enum CoinType {
591        Gas,
592        Usdc,
593    }
594
595    fn get_test_coin(id_hex_literal: Option<&str>, coin_type: CoinType) -> Coin {
596        let (arr, coin_type_string, balance, default_hex) = match coin_type {
597            CoinType::Gas => ([0; 32], GAS::type_().to_string(), 42, "0xA"),
598            CoinType::Usdc => (
599                [1; 32],
600                "0x168da5bf1f48dafc111b0a488fa454aca95e0b5e::usdc::USDC".to_string(),
601                24,
602                "0xB",
603            ),
604        };
605
606        let object_id = if let Some(literal) = id_hex_literal {
607            ObjectID::from_hex_literal(literal).unwrap()
608        } else {
609            ObjectID::from_hex_literal(default_hex).unwrap()
610        };
611
612        Coin {
613            coin_type: coin_type_string,
614            coin_object_id: object_id,
615            version: SequenceNumber::from_u64(1),
616            digest: ObjectDigest::from(arr),
617            balance,
618            previous_transaction: TransactionDigest::from(arr),
619        }
620    }
621
622    fn get_test_treasury_cap_peripherals(
623        package_id: ObjectID,
624    ) -> (String, StructTag, StructTag, TreasuryCap, Object) {
625        let coin_name = get_test_coin_type(package_id);
626        let input_coin_struct = parse_iota_struct_tag(&coin_name).expect("should not fail");
627        let treasury_cap_struct = TreasuryCap::type_(input_coin_struct.clone());
628        let treasury_cap = TreasuryCap {
629            id: UID::new(get_test_package_id()),
630            total_supply: Supply { value: 420 },
631        };
632        let treasury_cap_object =
633            Object::treasury_cap_for_testing(input_coin_struct.clone(), treasury_cap.clone());
634        (
635            coin_name,
636            input_coin_struct,
637            treasury_cap_struct,
638            treasury_cap,
639            treasury_cap_object,
640        )
641    }
642
643    mod get_coins_tests {
644        use super::{super::*, *};
645
646        // Success scenarios
647        #[tokio::test]
648        async fn test_gas_coin_no_cursor() {
649            let owner = get_test_owner();
650            let gas_coin = get_test_coin(None, CoinType::Gas);
651            let gas_coin_clone = gas_coin.clone();
652            let mut mock_state = MockStateRead::new();
653            mock_state
654                .expect_get_owned_coins()
655                .with(
656                    predicate::eq(owner),
657                    predicate::eq((GAS::type_().to_string(), ObjectID::ZERO)),
658                    predicate::eq(51),
659                    predicate::eq(true),
660                )
661                .return_once(move |_, _, _, _| Ok(vec![gas_coin_clone]));
662
663            let coin_read_api = CoinReadApi::new_for_tests(Arc::new(mock_state), None);
664            let response = coin_read_api.get_coins(owner, None, None, None).await;
665            assert!(response.is_ok());
666            let result = response.unwrap();
667            assert_eq!(
668                result,
669                CoinPage {
670                    data: vec![gas_coin.clone()],
671                    next_cursor: Some(gas_coin.coin_object_id),
672                    has_next_page: false,
673                }
674            );
675        }
676
677        #[tokio::test]
678        async fn test_gas_coin_with_cursor() {
679            let owner = get_test_owner();
680            let limit = 2;
681            let coins = vec![
682                get_test_coin(Some("0xA"), CoinType::Gas),
683                get_test_coin(Some("0xAA"), CoinType::Gas),
684                get_test_coin(Some("0xAAA"), CoinType::Gas),
685            ];
686            let coins_clone = coins.clone();
687            let mut mock_state = MockStateRead::new();
688            mock_state
689                .expect_get_owned_coins()
690                .with(
691                    predicate::eq(owner),
692                    predicate::eq((GAS::type_().to_string(), coins[0].coin_object_id)),
693                    predicate::eq(limit + 1),
694                    predicate::eq(true),
695                )
696                .return_once(move |_, _, _, _| Ok(coins_clone));
697
698            let coin_read_api = CoinReadApi::new_for_tests(Arc::new(mock_state), None);
699            let response = coin_read_api
700                .get_coins(owner, None, Some(coins[0].coin_object_id), Some(limit))
701                .await;
702            assert!(response.is_ok());
703            let result = response.unwrap();
704            assert_eq!(
705                result,
706                CoinPage {
707                    data: coins[..limit].to_vec(),
708                    next_cursor: Some(coins[limit - 1].coin_object_id),
709                    has_next_page: true,
710                }
711            );
712        }
713
714        #[tokio::test]
715        async fn test_coin_no_cursor() {
716            let coin = get_test_coin(None, CoinType::Usdc);
717            let coin_clone = coin.clone();
718            // Build request params
719            let owner = get_test_owner();
720            let coin_type = coin.coin_type.clone();
721
722            let coin_type_tag =
723                TypeTag::Struct(Box::new(parse_iota_struct_tag(&coin.coin_type).unwrap()));
724            let mut mock_state = MockStateRead::new();
725            mock_state
726                .expect_get_owned_coins()
727                .with(
728                    predicate::eq(owner),
729                    predicate::eq((coin_type_tag.to_string(), ObjectID::ZERO)),
730                    predicate::eq(51),
731                    predicate::eq(true),
732                )
733                .return_once(move |_, _, _, _| Ok(vec![coin_clone]));
734
735            let coin_read_api = CoinReadApi::new_for_tests(Arc::new(mock_state), None);
736            let response = coin_read_api
737                .get_coins(owner, Some(coin_type), None, None)
738                .await;
739
740            assert!(response.is_ok());
741            let result = response.unwrap();
742            assert_eq!(
743                result,
744                CoinPage {
745                    data: vec![coin.clone()],
746                    next_cursor: Some(coin.coin_object_id),
747                    has_next_page: false,
748                }
749            );
750        }
751
752        #[tokio::test]
753        async fn test_coin_with_cursor() {
754            let coins = vec![
755                get_test_coin(Some("0xB"), CoinType::Usdc),
756                get_test_coin(Some("0xBB"), CoinType::Usdc),
757                get_test_coin(Some("0xBBB"), CoinType::Usdc),
758            ];
759            let coins_clone = coins.clone();
760            // Build request params
761            let owner = get_test_owner();
762            let coin_type = coins[0].coin_type.clone();
763            let cursor = coins[0].coin_object_id;
764            let limit = 2;
765
766            let coin_type_tag = TypeTag::Struct(Box::new(
767                parse_iota_struct_tag(&coins[0].coin_type).unwrap(),
768            ));
769            let mut mock_state = MockStateRead::new();
770            mock_state
771                .expect_get_owned_coins()
772                .with(
773                    predicate::eq(owner),
774                    predicate::eq((coin_type_tag.to_string(), coins[0].coin_object_id)),
775                    predicate::eq(limit + 1),
776                    predicate::eq(true),
777                )
778                .return_once(move |_, _, _, _| Ok(coins_clone));
779
780            let coin_read_api = CoinReadApi::new_for_tests(Arc::new(mock_state), None);
781            let response = coin_read_api
782                .get_coins(owner, Some(coin_type), Some(cursor), Some(limit))
783                .await;
784
785            assert!(response.is_ok());
786            let result = response.unwrap();
787            assert_eq!(
788                result,
789                CoinPage {
790                    data: coins[..limit].to_vec(),
791                    next_cursor: Some(coins[limit - 1].coin_object_id),
792                    has_next_page: true,
793                }
794            );
795        }
796
797        // Expected error scenarios
798        #[tokio::test]
799        async fn test_invalid_coin_type() {
800            let owner = get_test_owner();
801            let coin_type = "0x2::invalid::struct::tag";
802            let mock_state = MockStateRead::new();
803            let coin_read_api = CoinReadApi::new_for_tests(Arc::new(mock_state), None);
804            let response = coin_read_api
805                .get_coins(owner, Some(coin_type.to_string()), None, None)
806                .await;
807
808            assert!(response.is_err());
809            let error_result = response.unwrap_err();
810            let expected = expect!["-32602"];
811            expected.assert_eq(&error_result.code().to_string());
812            let expected = expect![
813                "Invalid struct type: 0x2::invalid::struct::tag. Got error: Expected end of token stream. Got: ::"
814            ];
815            expected.assert_eq(error_result.message());
816        }
817
818        #[tokio::test]
819        async fn test_unrecognized_token() {
820            let owner = get_test_owner();
821            let coin_type = "0x2::iota:🤵";
822            let mock_state = MockStateRead::new();
823            let coin_read_api = CoinReadApi::new_for_tests(Arc::new(mock_state), None);
824            let response = coin_read_api
825                .get_coins(owner, Some(coin_type.to_string()), None, None)
826                .await;
827
828            assert!(response.is_err());
829            let error_result = response.unwrap_err();
830            let expected = expect!["-32602"];
831            expected.assert_eq(&error_result.code().to_string());
832            let expected =
833                expect!["Invalid struct type: 0x2::iota:🤵. Got error: unrecognized token: :🤵"];
834            expected.assert_eq(error_result.message());
835        }
836
837        // Unexpected error scenarios
838        #[tokio::test]
839        async fn test_get_coins_iterator_index_store_not_available() {
840            let owner = get_test_owner();
841            let coin_type = get_test_coin_type(get_test_package_id());
842            let mut mock_state = MockStateRead::new();
843            mock_state
844                .expect_get_owned_coins()
845                .returning(move |_, _, _, _| {
846                    Err(StateReadError::Client(
847                        IotaError::IndexStoreNotAvailable.into(),
848                    ))
849                });
850            let coin_read_api = CoinReadApi::new_for_tests(Arc::new(mock_state), None);
851            let response = coin_read_api
852                .get_coins(owner, Some(coin_type.to_string()), None, None)
853                .await;
854
855            assert!(response.is_err());
856            let error_result = response.unwrap_err();
857            assert_eq!(
858                error_result.code(),
859                jsonrpsee::types::error::INVALID_PARAMS_CODE
860            );
861            let expected = expect!["Index store not available on this Fullnode."];
862            expected.assert_eq(error_result.message());
863        }
864
865        #[tokio::test]
866        async fn test_get_coins_iterator_typed_store_error() {
867            let owner = get_test_owner();
868            let coin_type = get_test_coin_type(get_test_package_id());
869            let mut mock_state = MockStateRead::new();
870            mock_state
871                .expect_get_owned_coins()
872                .returning(move |_, _, _, _| {
873                    Err(IotaError::Storage("mock rocksdb error".to_string()).into())
874                });
875            let coin_read_api = CoinReadApi::new_for_tests(Arc::new(mock_state), None);
876            let response = coin_read_api
877                .get_coins(owner, Some(coin_type.to_string()), None, None)
878                .await;
879
880            assert!(response.is_err());
881            let error_result = response.unwrap_err();
882            assert_eq!(
883                error_result.code(),
884                jsonrpsee::types::error::INTERNAL_ERROR_CODE
885            );
886            let expected = expect!["Storage error: mock rocksdb error"];
887            expected.assert_eq(error_result.message());
888        }
889    }
890
891    mod get_all_coins_tests {
892        use iota_types::object::{MoveObject, Owner};
893
894        use super::{super::*, *};
895
896        // Success scenarios
897        #[tokio::test]
898        async fn test_no_cursor() {
899            let owner = get_test_owner();
900            let gas_coin = get_test_coin(None, CoinType::Gas);
901            let gas_coin_clone = gas_coin.clone();
902            let mut mock_state = MockStateRead::new();
903            mock_state
904                .expect_get_owned_coins()
905                .with(
906                    predicate::eq(owner),
907                    predicate::eq((String::from_utf8([0u8].to_vec()).unwrap(), ObjectID::ZERO)),
908                    predicate::eq(51),
909                    predicate::eq(false),
910                )
911                .return_once(move |_, _, _, _| Ok(vec![gas_coin_clone]));
912            let coin_read_api = CoinReadApi::new_for_tests(Arc::new(mock_state), None);
913            let response = coin_read_api
914                .get_all_coins(owner, None, Some(51))
915                .await
916                .unwrap();
917            assert_eq!(response.data.len(), 1);
918            assert_eq!(response.data[0], gas_coin);
919        }
920
921        #[tokio::test]
922        async fn test_with_cursor() {
923            let owner = get_test_owner();
924            let limit = 2;
925            let coins = vec![
926                get_test_coin(Some("0xA"), CoinType::Gas),
927                get_test_coin(Some("0xAA"), CoinType::Gas),
928                get_test_coin(Some("0xAAA"), CoinType::Gas),
929            ];
930            let coins_clone = coins.clone();
931            let coin_move_object = MoveObject::new_gas_coin(
932                coins[0].version,
933                coins[0].coin_object_id,
934                coins[0].balance,
935            );
936            let coin_object = Object::new_move(
937                coin_move_object,
938                Owner::Immutable,
939                coins[0].previous_transaction,
940            );
941            let mut mock_state = MockStateRead::new();
942            mock_state
943                .expect_get_object()
944                .return_once(move |_| Ok(Some(coin_object)));
945            mock_state
946                .expect_get_owned_coins()
947                .with(
948                    predicate::eq(owner),
949                    predicate::eq((coins[0].coin_type.clone(), coins[0].coin_object_id)),
950                    predicate::eq(limit + 1),
951                    predicate::eq(false),
952                )
953                .return_once(move |_, _, _, _| Ok(coins_clone));
954            let coin_read_api = CoinReadApi::new_for_tests(Arc::new(mock_state), None);
955            let response = coin_read_api
956                .get_all_coins(owner, Some(coins[0].coin_object_id), Some(limit))
957                .await
958                .unwrap();
959            assert_eq!(response.data.len(), limit);
960            assert_eq!(response.data, coins[..limit].to_vec());
961        }
962
963        // Expected error scenarios
964        #[tokio::test]
965        async fn test_object_is_not_coin() {
966            let owner = get_test_owner();
967            let object_id = get_test_package_id();
968            let (_, _, _, _, treasury_cap_object) = get_test_treasury_cap_peripherals(object_id);
969            let mut mock_state = MockStateRead::new();
970            mock_state.expect_get_object().returning(move |obj_id| {
971                if obj_id == &object_id {
972                    Ok(Some(treasury_cap_object.clone()))
973                } else {
974                    panic!("should not be called with any other object id")
975                }
976            });
977            let coin_read_api = CoinReadApi::new_for_tests(Arc::new(mock_state), None);
978            let response = coin_read_api
979                .get_all_coins(owner, Some(object_id), None)
980                .await;
981
982            assert!(response.is_err());
983            let error_result = response.unwrap_err();
984            assert_eq!(error_result.code(), -32602);
985            let expected = expect!["-32602"];
986            expected.assert_eq(&error_result.code().to_string());
987            let expected = expect!["cursor is not a coin"];
988            expected.assert_eq(error_result.message());
989        }
990
991        #[tokio::test]
992        async fn test_object_not_found() {
993            let owner = get_test_owner();
994            let object_id = get_test_package_id();
995            let mut mock_state = MockStateRead::new();
996            mock_state.expect_get_object().returning(move |_| Ok(None));
997
998            let coin_read_api = CoinReadApi::new_for_tests(Arc::new(mock_state), None);
999            let response = coin_read_api
1000                .get_all_coins(owner, Some(object_id), None)
1001                .await;
1002
1003            assert!(response.is_err());
1004            let error_result = response.unwrap_err();
1005            let expected = expect!["-32602"];
1006            expected.assert_eq(&error_result.code().to_string());
1007            let expected = expect!["cursor not found"];
1008            expected.assert_eq(error_result.message());
1009        }
1010    }
1011
1012    mod get_balance_tests {
1013
1014        use super::{super::*, *};
1015        // Success scenarios
1016        #[tokio::test]
1017        async fn test_gas_coin() {
1018            let owner = get_test_owner();
1019            let gas_coin = get_test_coin(None, CoinType::Gas);
1020            let gas_coin_clone = gas_coin.clone();
1021            let mut mock_state = MockStateRead::new();
1022            mock_state
1023                .expect_get_balance()
1024                .with(
1025                    predicate::eq(owner),
1026                    predicate::eq(get_test_coin_type_tag(gas_coin_clone.coin_type)),
1027                )
1028                .return_once(move |_, _| {
1029                    Ok(TotalBalance {
1030                        balance: 7,
1031                        num_coins: 9,
1032                    })
1033                });
1034            let coin_read_api = CoinReadApi::new_for_tests(Arc::new(mock_state), None);
1035            let response = coin_read_api.get_balance(owner, None).await;
1036
1037            assert!(response.is_ok());
1038            let result = response.unwrap();
1039            assert_eq!(
1040                result,
1041                Balance {
1042                    coin_type: gas_coin.coin_type,
1043                    coin_object_count: 9,
1044                    total_balance: 7,
1045                }
1046            );
1047        }
1048
1049        #[tokio::test]
1050        async fn test_with_coin_type() {
1051            let owner = get_test_owner();
1052            let coin = get_test_coin(None, CoinType::Usdc);
1053            let coin_clone = coin.clone();
1054            let mut mock_state = MockStateRead::new();
1055            mock_state
1056                .expect_get_balance()
1057                .with(
1058                    predicate::eq(owner),
1059                    predicate::eq(get_test_coin_type_tag(coin_clone.coin_type)),
1060                )
1061                .return_once(move |_, _| {
1062                    Ok(TotalBalance {
1063                        balance: 10,
1064                        num_coins: 11,
1065                    })
1066                });
1067            let coin_read_api = CoinReadApi::new_for_tests(Arc::new(mock_state), None);
1068            let response = coin_read_api
1069                .get_balance(owner, Some(coin.coin_type.clone()))
1070                .await;
1071
1072            assert!(response.is_ok());
1073            let result = response.unwrap();
1074            assert_eq!(
1075                result,
1076                Balance {
1077                    coin_type: coin.coin_type,
1078                    coin_object_count: 11,
1079                    total_balance: 10,
1080                }
1081            );
1082        }
1083
1084        // Expected error scenarios
1085        #[tokio::test]
1086        async fn test_invalid_coin_type() {
1087            let owner = get_test_owner();
1088            let coin_type = "0x2::invalid::struct::tag";
1089            let mock_state = MockStateRead::new();
1090            let coin_read_api = CoinReadApi::new_for_tests(Arc::new(mock_state), None);
1091            let response = coin_read_api
1092                .get_balance(owner, Some(coin_type.to_string()))
1093                .await;
1094
1095            assert!(response.is_err());
1096            let error_result = response.unwrap_err();
1097            let expected = expect!["-32602"];
1098            expected.assert_eq(&error_result.code().to_string());
1099            let expected = expect![
1100                "Invalid struct type: 0x2::invalid::struct::tag. Got error: Expected end of token stream. Got: ::"
1101            ];
1102            expected.assert_eq(error_result.message());
1103        }
1104
1105        // Unexpected error scenarios
1106        #[tokio::test]
1107        async fn test_get_balance_index_store_not_available() {
1108            let owner = get_test_owner();
1109            let coin_type = get_test_coin_type(get_test_package_id());
1110            let mut mock_state = MockStateRead::new();
1111            mock_state.expect_get_balance().returning(move |_, _| {
1112                Err(StateReadError::Client(
1113                    IotaError::IndexStoreNotAvailable.into(),
1114                ))
1115            });
1116            let coin_read_api = CoinReadApi::new_for_tests(Arc::new(mock_state), None);
1117            let response = coin_read_api
1118                .get_balance(owner, Some(coin_type.to_string()))
1119                .await;
1120
1121            assert!(response.is_err());
1122            let error_result = response.unwrap_err();
1123            assert_eq!(
1124                error_result.code(),
1125                jsonrpsee::types::error::INVALID_PARAMS_CODE
1126            );
1127            let expected = expect!["Index store not available on this Fullnode."];
1128            expected.assert_eq(error_result.message());
1129        }
1130
1131        #[tokio::test]
1132        async fn test_get_balance_execution_error() {
1133            // Validate that we handle and return an error message when we encounter an
1134            // unexpected error
1135            let owner = get_test_owner();
1136            let coin_type = get_test_coin_type(get_test_package_id());
1137            let mut mock_state = MockStateRead::new();
1138            mock_state.expect_get_balance().returning(move |_, _| {
1139                Err(IotaError::Execution("mock db error".to_string()).into())
1140            });
1141            let coin_read_api = CoinReadApi::new_for_tests(Arc::new(mock_state), None);
1142            let response = coin_read_api
1143                .get_balance(owner, Some(coin_type.to_string()))
1144                .await;
1145
1146            assert!(response.is_err());
1147            let error_result = response.unwrap_err();
1148
1149            assert_eq!(
1150                error_result.code(),
1151                jsonrpsee::types::error::INTERNAL_ERROR_CODE
1152            );
1153            let expected = expect!["Error executing mock db error"];
1154            expected.assert_eq(error_result.message());
1155        }
1156    }
1157
1158    mod get_all_balances_tests {
1159        use super::{super::*, *};
1160
1161        // Success scenarios
1162        #[tokio::test]
1163        async fn test_success_scenario() {
1164            let owner = get_test_owner();
1165            let gas_coin = get_test_coin(None, CoinType::Gas);
1166            let gas_coin_type_tag = get_test_coin_type_tag(gas_coin.coin_type.clone());
1167            let usdc_coin = get_test_coin(None, CoinType::Usdc);
1168            let usdc_coin_type_tag = get_test_coin_type_tag(usdc_coin.coin_type.clone());
1169            let mut mock_state = MockStateRead::new();
1170            mock_state
1171                .expect_get_all_balance()
1172                .with(predicate::eq(owner))
1173                .return_once(move |_| {
1174                    let mut hash_map = HashMap::new();
1175                    hash_map.insert(
1176                        gas_coin_type_tag,
1177                        TotalBalance {
1178                            balance: 7,
1179                            num_coins: 9,
1180                        },
1181                    );
1182                    hash_map.insert(
1183                        usdc_coin_type_tag,
1184                        TotalBalance {
1185                            balance: 10,
1186                            num_coins: 11,
1187                        },
1188                    );
1189                    Ok(Arc::new(hash_map))
1190                });
1191            let coin_read_api = CoinReadApi::new_for_tests(Arc::new(mock_state), None);
1192            let response = coin_read_api.get_all_balances(owner).await;
1193
1194            assert!(response.is_ok());
1195            let expected_result = vec![
1196                Balance {
1197                    coin_type: gas_coin.coin_type,
1198                    coin_object_count: 9,
1199                    total_balance: 7,
1200                },
1201                Balance {
1202                    coin_type: usdc_coin.coin_type,
1203                    coin_object_count: 11,
1204                    total_balance: 10,
1205                },
1206            ];
1207            // This is because the underlying result is a hashmap, so order is not
1208            // guaranteed
1209            let mut result = response.unwrap();
1210            for item in expected_result {
1211                if let Some(pos) = result.iter().position(|i| *i == item) {
1212                    result.remove(pos);
1213                } else {
1214                    panic!("{:?} not found in result", item);
1215                }
1216            }
1217            assert!(result.is_empty());
1218        }
1219
1220        // Unexpected error scenarios
1221        #[tokio::test]
1222        async fn test_index_store_not_available() {
1223            let owner = get_test_owner();
1224            let mut mock_state = MockStateRead::new();
1225            mock_state.expect_get_all_balance().returning(move |_| {
1226                Err(StateReadError::Client(
1227                    IotaError::IndexStoreNotAvailable.into(),
1228                ))
1229            });
1230            let coin_read_api = CoinReadApi::new_for_tests(Arc::new(mock_state), None);
1231            let response = coin_read_api.get_all_balances(owner).await;
1232
1233            assert!(response.is_err());
1234            let error_result = response.unwrap_err();
1235            assert_eq!(
1236                error_result.code(),
1237                jsonrpsee::types::error::INVALID_PARAMS_CODE
1238            );
1239            let expected = expect!["Index store not available on this Fullnode."];
1240            expected.assert_eq(error_result.message());
1241        }
1242    }
1243
1244    mod get_coin_metadata_tests {
1245        use iota_types::id::UID;
1246        use mockall::predicate;
1247
1248        use super::{super::*, *};
1249
1250        // Success scenarios
1251        #[tokio::test]
1252        async fn test_valid_coin_metadata_object() {
1253            let package_id = get_test_package_id();
1254            let coin_name = get_test_coin_type(package_id);
1255            let input_coin_struct = parse_iota_struct_tag(&coin_name).expect("should not fail");
1256            let coin_metadata_struct = CoinMetadata::type_(input_coin_struct.clone());
1257            let coin_metadata = CoinMetadata {
1258                id: UID::new(get_test_package_id()),
1259                decimals: 2,
1260                name: "test_coin".to_string(),
1261                symbol: "TEST".to_string(),
1262                description: "test coin".to_string(),
1263                icon_url: Some("unit.test.io".to_string()),
1264            };
1265            let coin_metadata_object =
1266                Object::coin_metadata_for_testing(input_coin_struct.clone(), coin_metadata);
1267            let metadata = IotaCoinMetadata::try_from(coin_metadata_object.clone()).unwrap();
1268            let mut mock_internal = MockCoinReadInternal::new();
1269            // return TreasuryCap instead of CoinMetadata to set up test
1270            mock_internal
1271                .expect_find_package_object()
1272                .with(predicate::always(), predicate::eq(coin_metadata_struct))
1273                .return_once(move |object_id, _| {
1274                    if object_id == &package_id {
1275                        Ok(coin_metadata_object)
1276                    } else {
1277                        panic!("should not be called with any other object id")
1278                    }
1279                });
1280
1281            let coin_read_api = CoinReadApi {
1282                internal: Box::new(mock_internal),
1283                unlocks_store: MainnetUnlocksStore::new().unwrap(),
1284            };
1285
1286            let response = coin_read_api.get_coin_metadata(coin_name.clone()).await;
1287            assert!(response.is_ok());
1288            let result = response.unwrap().unwrap();
1289            assert_eq!(result, metadata);
1290        }
1291
1292        #[tokio::test]
1293        async fn test_object_not_found() {
1294            let transaction_digest = TransactionDigest::from([0; 32]);
1295            let transaction_effects = TransactionEffects::default();
1296
1297            let mut mock_state = MockStateRead::new();
1298            mock_state
1299                .expect_find_publish_txn_digest()
1300                .return_once(move |_| Ok(transaction_digest));
1301            mock_state
1302                .expect_get_executed_transaction_and_effects()
1303                .return_once(move |_, _| Ok((create_fake_transaction(), transaction_effects)));
1304
1305            let coin_read_api = CoinReadApi::new_for_tests(Arc::new(mock_state), None);
1306            let response = coin_read_api
1307                .get_coin_metadata("0x2::iota::IOTA".to_string())
1308                .await;
1309
1310            assert!(response.is_ok());
1311            let result = response.unwrap();
1312            assert_eq!(result, None);
1313        }
1314
1315        #[tokio::test]
1316        async fn test_find_package_object_not_iota_coin_metadata() {
1317            let package_id = get_test_package_id();
1318            let coin_name = get_test_coin_type(package_id);
1319            let input_coin_struct = parse_iota_struct_tag(&coin_name).expect("should not fail");
1320            let coin_metadata_struct = CoinMetadata::type_(input_coin_struct.clone());
1321            let treasury_cap = TreasuryCap {
1322                id: UID::new(get_test_package_id()),
1323                total_supply: Supply { value: 420 },
1324            };
1325            let treasury_cap_object =
1326                Object::treasury_cap_for_testing(input_coin_struct.clone(), treasury_cap);
1327            let mut mock_internal = MockCoinReadInternal::new();
1328            // return TreasuryCap instead of CoinMetadata to set up test
1329            mock_internal
1330                .expect_find_package_object()
1331                .with(predicate::always(), predicate::eq(coin_metadata_struct))
1332                .returning(move |object_id, _| {
1333                    if object_id == &package_id {
1334                        Ok(treasury_cap_object.clone())
1335                    } else {
1336                        panic!("should not be called with any other object id")
1337                    }
1338                });
1339
1340            let coin_read_api = CoinReadApi {
1341                internal: Box::new(mock_internal),
1342                unlocks_store: MainnetUnlocksStore::new().unwrap(),
1343            };
1344
1345            let response = coin_read_api.get_coin_metadata(coin_name.clone()).await;
1346            assert!(response.is_ok());
1347            let result = response.unwrap();
1348            assert!(result.is_none());
1349        }
1350    }
1351
1352    mod get_total_supply_tests {
1353        use iota_types::{
1354            collection_types::VecMap,
1355            gas_coin::IotaTreasuryCap,
1356            id::UID,
1357            iota_system_state::{
1358                IotaSystemState,
1359                iota_system_state_inner_v1::{StorageFundV1, SystemParametersV1},
1360                iota_system_state_inner_v2::{IotaSystemStateV2, ValidatorSetV2},
1361            },
1362        };
1363        use mockall::predicate;
1364
1365        use super::{super::*, *};
1366
1367        #[tokio::test]
1368        async fn test_success_response_for_gas_coin() {
1369            let coin_type = "0x2::iota::IOTA";
1370
1371            let mut mock_state = MockStateRead::new();
1372            mock_state.expect_get_system_state().returning(move || {
1373                let mut state = default_system_state();
1374                state.iota_treasury_cap.inner.total_supply.value = 42;
1375
1376                Ok(IotaSystemState::V2(state))
1377            });
1378
1379            let coin_read_api = CoinReadApi::new_for_tests(Arc::new(mock_state), None);
1380
1381            let response = coin_read_api.get_total_supply(coin_type.to_string()).await;
1382
1383            let supply = response.unwrap();
1384            assert_eq!(supply.value, 42);
1385        }
1386
1387        #[tokio::test]
1388        async fn test_success_response_for_other_coin() {
1389            let package_id = get_test_package_id();
1390            let (coin_name, _, treasury_cap_struct, _, treasury_cap_object) =
1391                get_test_treasury_cap_peripherals(package_id);
1392            let mut mock_internal = MockCoinReadInternal::new();
1393            mock_internal
1394                .expect_find_package_object()
1395                .with(predicate::always(), predicate::eq(treasury_cap_struct))
1396                .returning(move |object_id, _| {
1397                    if object_id == &package_id {
1398                        Ok(treasury_cap_object.clone())
1399                    } else {
1400                        panic!("should not be called with any other object id")
1401                    }
1402                });
1403            let coin_read_api = CoinReadApi {
1404                internal: Box::new(mock_internal),
1405                unlocks_store: MainnetUnlocksStore::new().unwrap(),
1406            };
1407
1408            let response = coin_read_api.get_total_supply(coin_name.clone()).await;
1409
1410            assert!(response.is_ok());
1411            let result = response.unwrap();
1412            let expected = expect!["420"];
1413            expected.assert_eq(&result.value.to_string());
1414        }
1415
1416        #[tokio::test]
1417        async fn test_object_not_found() {
1418            let package_id = get_test_package_id();
1419            let (coin_name, _, _, _, _) = get_test_treasury_cap_peripherals(package_id);
1420            let transaction_digest = TransactionDigest::from([0; 32]);
1421            let transaction_effects = TransactionEffects::default();
1422
1423            let mut mock_state = MockStateRead::new();
1424            mock_state
1425                .expect_find_publish_txn_digest()
1426                .return_once(move |_| Ok(transaction_digest));
1427            mock_state
1428                .expect_get_executed_transaction_and_effects()
1429                .return_once(move |_, _| Ok((create_fake_transaction(), transaction_effects)));
1430
1431            let coin_read_api = CoinReadApi::new_for_tests(Arc::new(mock_state), None);
1432            let response = coin_read_api.get_total_supply(coin_name.clone()).await;
1433
1434            assert!(response.is_err());
1435            let error_result = response.unwrap_err();
1436            let expected = expect!["-32602"];
1437            expected.assert_eq(&error_result.code().to_string());
1438            let expected = expect![
1439                "Cannot find object [0x2::coin::TreasuryCap<0xf::test_coin::TEST_COIN>] from [0x000000000000000000000000000000000000000000000000000000000000000f] package event."
1440            ];
1441            expected.assert_eq(error_result.message());
1442        }
1443
1444        #[tokio::test]
1445        async fn test_find_package_object_not_treasury_cap() {
1446            let package_id = get_test_package_id();
1447            let (coin_name, input_coin_struct, treasury_cap_struct, _, _) =
1448                get_test_treasury_cap_peripherals(package_id);
1449            let coin_metadata = CoinMetadata {
1450                id: UID::new(get_test_package_id()),
1451                decimals: 2,
1452                name: "test_coin".to_string(),
1453                symbol: "TEST".to_string(),
1454                description: "test coin".to_string(),
1455                icon_url: None,
1456            };
1457            let coin_metadata_object =
1458                Object::coin_metadata_for_testing(input_coin_struct.clone(), coin_metadata);
1459            let mut mock_internal = MockCoinReadInternal::new();
1460            mock_internal
1461                .expect_find_package_object()
1462                .with(predicate::always(), predicate::eq(treasury_cap_struct))
1463                .returning(move |object_id, _| {
1464                    if object_id == &package_id {
1465                        Ok(coin_metadata_object.clone())
1466                    } else {
1467                        panic!("should not be called with any other object id")
1468                    }
1469                });
1470
1471            let coin_read_api = CoinReadApi {
1472                internal: Box::new(mock_internal),
1473                unlocks_store: MainnetUnlocksStore::new().unwrap(),
1474            };
1475
1476            let response = coin_read_api.get_total_supply(coin_name.clone()).await;
1477            let error_result = response.unwrap_err();
1478            assert_eq!(
1479                error_result.code(),
1480                jsonrpsee::types::error::CALL_EXECUTION_FAILED_CODE
1481            );
1482            let expected = expect![
1483                "Failure deserializing object in the requested format: \"Unable to deserialize TreasuryCap object: remaining input\""
1484            ];
1485            expected.assert_eq(error_result.message());
1486        }
1487
1488        fn default_system_state() -> IotaSystemStateV2 {
1489            IotaSystemStateV2 {
1490                epoch: Default::default(),
1491                protocol_version: Default::default(),
1492                system_state_version: Default::default(),
1493                iota_treasury_cap: IotaTreasuryCap {
1494                    inner: TreasuryCap {
1495                        id: UID::new(ObjectID::random()),
1496                        total_supply: Supply {
1497                            value: Default::default(),
1498                        },
1499                    },
1500                },
1501                validators: ValidatorSetV2 {
1502                    total_stake: Default::default(),
1503                    active_validators: Default::default(),
1504                    committee_members: Default::default(),
1505                    pending_active_validators: Default::default(),
1506                    pending_removals: Default::default(),
1507                    staking_pool_mappings: Default::default(),
1508                    inactive_validators: Default::default(),
1509                    validator_candidates: Default::default(),
1510                    at_risk_validators: VecMap {
1511                        contents: Default::default(),
1512                    },
1513                    extra_fields: Default::default(),
1514                },
1515                storage_fund: StorageFundV1 {
1516                    total_object_storage_rebates: iota_types::balance::Balance::new(
1517                        Default::default(),
1518                    ),
1519                    non_refundable_balance: iota_types::balance::Balance::new(Default::default()),
1520                },
1521                parameters: SystemParametersV1 {
1522                    epoch_duration_ms: Default::default(),
1523                    min_validator_count: Default::default(),
1524                    max_validator_count: Default::default(),
1525                    min_validator_joining_stake: Default::default(),
1526                    validator_low_stake_threshold: Default::default(),
1527                    validator_very_low_stake_threshold: Default::default(),
1528                    validator_low_stake_grace_period: Default::default(),
1529                    extra_fields: Default::default(),
1530                },
1531                iota_system_admin_cap: Default::default(),
1532                reference_gas_price: Default::default(),
1533                validator_report_records: VecMap {
1534                    contents: Default::default(),
1535                },
1536                safe_mode: Default::default(),
1537                safe_mode_storage_charges: iota_types::balance::Balance::new(Default::default()),
1538                safe_mode_computation_charges: iota_types::balance::Balance::new(Default::default()),
1539                safe_mode_computation_charges_burned: Default::default(),
1540                safe_mode_storage_rebates: Default::default(),
1541                safe_mode_non_refundable_storage_fee: Default::default(),
1542                epoch_start_timestamp_ms: Default::default(),
1543                extra_fields: Default::default(),
1544            }
1545        }
1546    }
1547}