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.report(limit as u64);
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.get_coins_result_size.report(data.len() as u64);
463        self.metrics
464            .get_coins_result_size_total
465            .inc_by(data.len() as u64);
466        let next_cursor = data.last().map(|coin| coin.coin_object_id);
467        Ok(CoinPage {
468            data,
469            next_cursor,
470            has_next_page,
471        })
472    }
473}
474
475#[cfg(test)]
476mod tests {
477    use expect_test::expect;
478    use iota_json_rpc_types::Coin;
479    use iota_storage::{
480        key_value_store::{
481            KVStoreCheckpointData, KVStoreTransactionData, TransactionKeyValueStoreTrait,
482        },
483        key_value_store_metrics::KeyValueStoreMetrics,
484    };
485    use iota_types::{
486        TypeTag,
487        balance::Supply,
488        base_types::{IotaAddress, ObjectID, SequenceNumber},
489        coin::TreasuryCap,
490        digests::{ObjectDigest, TransactionDigest},
491        effects::{TransactionEffects, TransactionEvents},
492        error::{IotaError, IotaResult},
493        gas_coin::GAS,
494        id::UID,
495        messages_checkpoint::{CheckpointDigest, CheckpointSequenceNumber},
496        object::Object,
497        parse_iota_struct_tag,
498        utils::create_fake_transaction,
499    };
500    use mockall::{mock, predicate};
501    use move_core_types::{account_address::AccountAddress, language_storage::StructTag};
502
503    use super::*;
504    use crate::authority_state::{MockStateRead, StateReadError};
505
506    mock! {
507        pub KeyValueStore {}
508        #[async_trait]
509        impl TransactionKeyValueStoreTrait for KeyValueStore {
510            async fn multi_get(
511                &self,
512                transaction_keys: &[TransactionDigest],
513                effects_keys: &[TransactionDigest],
514            ) -> IotaResult<KVStoreTransactionData>;
515
516            async fn multi_get_checkpoints(
517                &self,
518                checkpoint_summaries: &[CheckpointSequenceNumber],
519                checkpoint_contents: &[CheckpointSequenceNumber],
520                checkpoint_summaries_by_digest: &[CheckpointDigest],
521            ) -> IotaResult<KVStoreCheckpointData>;
522
523            async fn get_transaction_perpetual_checkpoint(
524                &self,
525                digest: TransactionDigest,
526            ) -> IotaResult<Option<CheckpointSequenceNumber>>;
527
528            async fn get_object(&self, object_id: ObjectID, version: SequenceNumber) -> IotaResult<Option<Object>>;
529
530            async fn multi_get_transactions_perpetual_checkpoints(
531                &self,
532                digests: &[TransactionDigest],
533            ) -> IotaResult<Vec<Option<CheckpointSequenceNumber>>>;
534
535            async fn multi_get_events_by_tx_digests(
536                &self,
537                digests: &[TransactionDigest]
538            ) -> IotaResult<Vec<Option<TransactionEvents>>>;
539        }
540    }
541
542    impl CoinReadInternalImpl {
543        pub fn new_for_tests(
544            state: Arc<MockStateRead>,
545            kv_store: Option<Arc<MockKeyValueStore>>,
546        ) -> Self {
547            let kv_store = kv_store.unwrap_or_else(|| Arc::new(MockKeyValueStore::new()));
548            let metrics = KeyValueStoreMetrics::new_for_tests();
549            let transaction_kv_store =
550                Arc::new(TransactionKeyValueStore::new("rocksdb", metrics, kv_store));
551            Self {
552                state,
553                transaction_kv_store,
554                metrics: Arc::new(JsonRpcMetrics::new_for_tests()),
555            }
556        }
557    }
558
559    impl CoinReadApi {
560        pub fn new_for_tests(
561            state: Arc<MockStateRead>,
562            kv_store: Option<Arc<MockKeyValueStore>>,
563        ) -> Self {
564            let kv_store = kv_store.unwrap_or_else(|| Arc::new(MockKeyValueStore::new()));
565            Self {
566                internal: Box::new(CoinReadInternalImpl::new_for_tests(state, Some(kv_store))),
567                unlocks_store: MainnetUnlocksStore::new().unwrap(),
568            }
569        }
570    }
571
572    fn get_test_owner() -> IotaAddress {
573        AccountAddress::ONE.into()
574    }
575
576    fn get_test_package_id() -> ObjectID {
577        ObjectID::from_hex_literal("0xf").unwrap()
578    }
579
580    fn get_test_coin_type(package_id: ObjectID) -> String {
581        format!("{}::test_coin::TEST_COIN", package_id)
582    }
583
584    fn get_test_coin_type_tag(coin_type: String) -> TypeTag {
585        TypeTag::Struct(Box::new(parse_iota_struct_tag(&coin_type).unwrap()))
586    }
587
588    enum CoinType {
589        Gas,
590        Usdc,
591    }
592
593    fn get_test_coin(id_hex_literal: Option<&str>, coin_type: CoinType) -> Coin {
594        let (arr, coin_type_string, balance, default_hex) = match coin_type {
595            CoinType::Gas => ([0; 32], GAS::type_().to_string(), 42, "0xA"),
596            CoinType::Usdc => (
597                [1; 32],
598                "0x168da5bf1f48dafc111b0a488fa454aca95e0b5e::usdc::USDC".to_string(),
599                24,
600                "0xB",
601            ),
602        };
603
604        let object_id = if let Some(literal) = id_hex_literal {
605            ObjectID::from_hex_literal(literal).unwrap()
606        } else {
607            ObjectID::from_hex_literal(default_hex).unwrap()
608        };
609
610        Coin {
611            coin_type: coin_type_string,
612            coin_object_id: object_id,
613            version: SequenceNumber::from_u64(1),
614            digest: ObjectDigest::from(arr),
615            balance,
616            previous_transaction: TransactionDigest::from(arr),
617        }
618    }
619
620    fn get_test_treasury_cap_peripherals(
621        package_id: ObjectID,
622    ) -> (String, StructTag, StructTag, TreasuryCap, Object) {
623        let coin_name = get_test_coin_type(package_id);
624        let input_coin_struct = parse_iota_struct_tag(&coin_name).expect("should not fail");
625        let treasury_cap_struct = TreasuryCap::type_(input_coin_struct.clone());
626        let treasury_cap = TreasuryCap {
627            id: UID::new(get_test_package_id()),
628            total_supply: Supply { value: 420 },
629        };
630        let treasury_cap_object =
631            Object::treasury_cap_for_testing(input_coin_struct.clone(), treasury_cap.clone());
632        (
633            coin_name,
634            input_coin_struct,
635            treasury_cap_struct,
636            treasury_cap,
637            treasury_cap_object,
638        )
639    }
640
641    mod get_coins_tests {
642        use super::{super::*, *};
643
644        // Success scenarios
645        #[tokio::test]
646        async fn test_gas_coin_no_cursor() {
647            let owner = get_test_owner();
648            let gas_coin = get_test_coin(None, CoinType::Gas);
649            let gas_coin_clone = gas_coin.clone();
650            let mut mock_state = MockStateRead::new();
651            mock_state
652                .expect_get_owned_coins()
653                .with(
654                    predicate::eq(owner),
655                    predicate::eq((GAS::type_().to_string(), ObjectID::ZERO)),
656                    predicate::eq(51),
657                    predicate::eq(true),
658                )
659                .return_once(move |_, _, _, _| Ok(vec![gas_coin_clone]));
660
661            let coin_read_api = CoinReadApi::new_for_tests(Arc::new(mock_state), None);
662            let response = coin_read_api.get_coins(owner, None, None, None).await;
663            assert!(response.is_ok());
664            let result = response.unwrap();
665            assert_eq!(
666                result,
667                CoinPage {
668                    data: vec![gas_coin.clone()],
669                    next_cursor: Some(gas_coin.coin_object_id),
670                    has_next_page: false,
671                }
672            );
673        }
674
675        #[tokio::test]
676        async fn test_gas_coin_with_cursor() {
677            let owner = get_test_owner();
678            let limit = 2;
679            let coins = vec![
680                get_test_coin(Some("0xA"), CoinType::Gas),
681                get_test_coin(Some("0xAA"), CoinType::Gas),
682                get_test_coin(Some("0xAAA"), CoinType::Gas),
683            ];
684            let coins_clone = coins.clone();
685            let mut mock_state = MockStateRead::new();
686            mock_state
687                .expect_get_owned_coins()
688                .with(
689                    predicate::eq(owner),
690                    predicate::eq((GAS::type_().to_string(), coins[0].coin_object_id)),
691                    predicate::eq(limit + 1),
692                    predicate::eq(true),
693                )
694                .return_once(move |_, _, _, _| Ok(coins_clone));
695
696            let coin_read_api = CoinReadApi::new_for_tests(Arc::new(mock_state), None);
697            let response = coin_read_api
698                .get_coins(owner, None, Some(coins[0].coin_object_id), Some(limit))
699                .await;
700            assert!(response.is_ok());
701            let result = response.unwrap();
702            assert_eq!(
703                result,
704                CoinPage {
705                    data: coins[..limit].to_vec(),
706                    next_cursor: Some(coins[limit - 1].coin_object_id),
707                    has_next_page: true,
708                }
709            );
710        }
711
712        #[tokio::test]
713        async fn test_coin_no_cursor() {
714            let coin = get_test_coin(None, CoinType::Usdc);
715            let coin_clone = coin.clone();
716            // Build request params
717            let owner = get_test_owner();
718            let coin_type = coin.coin_type.clone();
719
720            let coin_type_tag =
721                TypeTag::Struct(Box::new(parse_iota_struct_tag(&coin.coin_type).unwrap()));
722            let mut mock_state = MockStateRead::new();
723            mock_state
724                .expect_get_owned_coins()
725                .with(
726                    predicate::eq(owner),
727                    predicate::eq((coin_type_tag.to_string(), ObjectID::ZERO)),
728                    predicate::eq(51),
729                    predicate::eq(true),
730                )
731                .return_once(move |_, _, _, _| Ok(vec![coin_clone]));
732
733            let coin_read_api = CoinReadApi::new_for_tests(Arc::new(mock_state), None);
734            let response = coin_read_api
735                .get_coins(owner, Some(coin_type), None, None)
736                .await;
737
738            assert!(response.is_ok());
739            let result = response.unwrap();
740            assert_eq!(
741                result,
742                CoinPage {
743                    data: vec![coin.clone()],
744                    next_cursor: Some(coin.coin_object_id),
745                    has_next_page: false,
746                }
747            );
748        }
749
750        #[tokio::test]
751        async fn test_coin_with_cursor() {
752            let coins = vec![
753                get_test_coin(Some("0xB"), CoinType::Usdc),
754                get_test_coin(Some("0xBB"), CoinType::Usdc),
755                get_test_coin(Some("0xBBB"), CoinType::Usdc),
756            ];
757            let coins_clone = coins.clone();
758            // Build request params
759            let owner = get_test_owner();
760            let coin_type = coins[0].coin_type.clone();
761            let cursor = coins[0].coin_object_id;
762            let limit = 2;
763
764            let coin_type_tag = TypeTag::Struct(Box::new(
765                parse_iota_struct_tag(&coins[0].coin_type).unwrap(),
766            ));
767            let mut mock_state = MockStateRead::new();
768            mock_state
769                .expect_get_owned_coins()
770                .with(
771                    predicate::eq(owner),
772                    predicate::eq((coin_type_tag.to_string(), coins[0].coin_object_id)),
773                    predicate::eq(limit + 1),
774                    predicate::eq(true),
775                )
776                .return_once(move |_, _, _, _| Ok(coins_clone));
777
778            let coin_read_api = CoinReadApi::new_for_tests(Arc::new(mock_state), None);
779            let response = coin_read_api
780                .get_coins(owner, Some(coin_type), Some(cursor), Some(limit))
781                .await;
782
783            assert!(response.is_ok());
784            let result = response.unwrap();
785            assert_eq!(
786                result,
787                CoinPage {
788                    data: coins[..limit].to_vec(),
789                    next_cursor: Some(coins[limit - 1].coin_object_id),
790                    has_next_page: true,
791                }
792            );
793        }
794
795        // Expected error scenarios
796        #[tokio::test]
797        async fn test_invalid_coin_type() {
798            let owner = get_test_owner();
799            let coin_type = "0x2::invalid::struct::tag";
800            let mock_state = MockStateRead::new();
801            let coin_read_api = CoinReadApi::new_for_tests(Arc::new(mock_state), None);
802            let response = coin_read_api
803                .get_coins(owner, Some(coin_type.to_string()), None, None)
804                .await;
805
806            assert!(response.is_err());
807            let error_result = response.unwrap_err();
808            let expected = expect!["-32602"];
809            expected.assert_eq(&error_result.code().to_string());
810            let expected = expect![
811                "Invalid struct type: 0x2::invalid::struct::tag. Got error: Expected end of token stream. Got: ::"
812            ];
813            expected.assert_eq(error_result.message());
814        }
815
816        #[tokio::test]
817        async fn test_unrecognized_token() {
818            let owner = get_test_owner();
819            let coin_type = "0x2::iota:🤵";
820            let mock_state = MockStateRead::new();
821            let coin_read_api = CoinReadApi::new_for_tests(Arc::new(mock_state), None);
822            let response = coin_read_api
823                .get_coins(owner, Some(coin_type.to_string()), None, None)
824                .await;
825
826            assert!(response.is_err());
827            let error_result = response.unwrap_err();
828            let expected = expect!["-32602"];
829            expected.assert_eq(&error_result.code().to_string());
830            let expected =
831                expect!["Invalid struct type: 0x2::iota:🤵. Got error: unrecognized token: :🤵"];
832            expected.assert_eq(error_result.message());
833        }
834
835        // Unexpected error scenarios
836        #[tokio::test]
837        async fn test_get_coins_iterator_index_store_not_available() {
838            let owner = get_test_owner();
839            let coin_type = get_test_coin_type(get_test_package_id());
840            let mut mock_state = MockStateRead::new();
841            mock_state
842                .expect_get_owned_coins()
843                .returning(move |_, _, _, _| {
844                    Err(StateReadError::Client(
845                        IotaError::IndexStoreNotAvailable.into(),
846                    ))
847                });
848            let coin_read_api = CoinReadApi::new_for_tests(Arc::new(mock_state), None);
849            let response = coin_read_api
850                .get_coins(owner, Some(coin_type.to_string()), None, None)
851                .await;
852
853            assert!(response.is_err());
854            let error_result = response.unwrap_err();
855            assert_eq!(
856                error_result.code(),
857                jsonrpsee::types::error::INVALID_PARAMS_CODE
858            );
859            let expected = expect!["Index store not available on this Fullnode."];
860            expected.assert_eq(error_result.message());
861        }
862
863        #[tokio::test]
864        async fn test_get_coins_iterator_typed_store_error() {
865            let owner = get_test_owner();
866            let coin_type = get_test_coin_type(get_test_package_id());
867            let mut mock_state = MockStateRead::new();
868            mock_state
869                .expect_get_owned_coins()
870                .returning(move |_, _, _, _| {
871                    Err(IotaError::Storage("mock rocksdb error".to_string()).into())
872                });
873            let coin_read_api = CoinReadApi::new_for_tests(Arc::new(mock_state), None);
874            let response = coin_read_api
875                .get_coins(owner, Some(coin_type.to_string()), None, None)
876                .await;
877
878            assert!(response.is_err());
879            let error_result = response.unwrap_err();
880            assert_eq!(
881                error_result.code(),
882                jsonrpsee::types::error::INTERNAL_ERROR_CODE
883            );
884            let expected = expect!["Storage error: mock rocksdb error"];
885            expected.assert_eq(error_result.message());
886        }
887    }
888
889    mod get_all_coins_tests {
890        use iota_types::object::{MoveObject, Owner};
891
892        use super::{super::*, *};
893
894        // Success scenarios
895        #[tokio::test]
896        async fn test_no_cursor() {
897            let owner = get_test_owner();
898            let gas_coin = get_test_coin(None, CoinType::Gas);
899            let gas_coin_clone = gas_coin.clone();
900            let mut mock_state = MockStateRead::new();
901            mock_state
902                .expect_get_owned_coins()
903                .with(
904                    predicate::eq(owner),
905                    predicate::eq((String::from_utf8([0u8].to_vec()).unwrap(), ObjectID::ZERO)),
906                    predicate::eq(51),
907                    predicate::eq(false),
908                )
909                .return_once(move |_, _, _, _| Ok(vec![gas_coin_clone]));
910            let coin_read_api = CoinReadApi::new_for_tests(Arc::new(mock_state), None);
911            let response = coin_read_api
912                .get_all_coins(owner, None, Some(51))
913                .await
914                .unwrap();
915            assert_eq!(response.data.len(), 1);
916            assert_eq!(response.data[0], gas_coin);
917        }
918
919        #[tokio::test]
920        async fn test_with_cursor() {
921            let owner = get_test_owner();
922            let limit = 2;
923            let coins = vec![
924                get_test_coin(Some("0xA"), CoinType::Gas),
925                get_test_coin(Some("0xAA"), CoinType::Gas),
926                get_test_coin(Some("0xAAA"), CoinType::Gas),
927            ];
928            let coins_clone = coins.clone();
929            let coin_move_object = MoveObject::new_gas_coin(
930                coins[0].version,
931                coins[0].coin_object_id,
932                coins[0].balance,
933            );
934            let coin_object = Object::new_move(
935                coin_move_object,
936                Owner::Immutable,
937                coins[0].previous_transaction,
938            );
939            let mut mock_state = MockStateRead::new();
940            mock_state
941                .expect_get_object()
942                .return_once(move |_| Ok(Some(coin_object)));
943            mock_state
944                .expect_get_owned_coins()
945                .with(
946                    predicate::eq(owner),
947                    predicate::eq((coins[0].coin_type.clone(), coins[0].coin_object_id)),
948                    predicate::eq(limit + 1),
949                    predicate::eq(false),
950                )
951                .return_once(move |_, _, _, _| Ok(coins_clone));
952            let coin_read_api = CoinReadApi::new_for_tests(Arc::new(mock_state), None);
953            let response = coin_read_api
954                .get_all_coins(owner, Some(coins[0].coin_object_id), Some(limit))
955                .await
956                .unwrap();
957            assert_eq!(response.data.len(), limit);
958            assert_eq!(response.data, coins[..limit].to_vec());
959        }
960
961        // Expected error scenarios
962        #[tokio::test]
963        async fn test_object_is_not_coin() {
964            let owner = get_test_owner();
965            let object_id = get_test_package_id();
966            let (_, _, _, _, treasury_cap_object) = get_test_treasury_cap_peripherals(object_id);
967            let mut mock_state = MockStateRead::new();
968            mock_state.expect_get_object().returning(move |obj_id| {
969                if obj_id == &object_id {
970                    Ok(Some(treasury_cap_object.clone()))
971                } else {
972                    panic!("should not be called with any other object id")
973                }
974            });
975            let coin_read_api = CoinReadApi::new_for_tests(Arc::new(mock_state), None);
976            let response = coin_read_api
977                .get_all_coins(owner, Some(object_id), None)
978                .await;
979
980            assert!(response.is_err());
981            let error_result = response.unwrap_err();
982            assert_eq!(error_result.code(), -32602);
983            let expected = expect!["-32602"];
984            expected.assert_eq(&error_result.code().to_string());
985            let expected = expect!["cursor is not a coin"];
986            expected.assert_eq(error_result.message());
987        }
988
989        #[tokio::test]
990        async fn test_object_not_found() {
991            let owner = get_test_owner();
992            let object_id = get_test_package_id();
993            let mut mock_state = MockStateRead::new();
994            mock_state.expect_get_object().returning(move |_| Ok(None));
995
996            let coin_read_api = CoinReadApi::new_for_tests(Arc::new(mock_state), None);
997            let response = coin_read_api
998                .get_all_coins(owner, Some(object_id), None)
999                .await;
1000
1001            assert!(response.is_err());
1002            let error_result = response.unwrap_err();
1003            let expected = expect!["-32602"];
1004            expected.assert_eq(&error_result.code().to_string());
1005            let expected = expect!["cursor not found"];
1006            expected.assert_eq(error_result.message());
1007        }
1008    }
1009
1010    mod get_balance_tests {
1011
1012        use super::{super::*, *};
1013        // Success scenarios
1014        #[tokio::test]
1015        async fn test_gas_coin() {
1016            let owner = get_test_owner();
1017            let gas_coin = get_test_coin(None, CoinType::Gas);
1018            let gas_coin_clone = gas_coin.clone();
1019            let mut mock_state = MockStateRead::new();
1020            mock_state
1021                .expect_get_balance()
1022                .with(
1023                    predicate::eq(owner),
1024                    predicate::eq(get_test_coin_type_tag(gas_coin_clone.coin_type)),
1025                )
1026                .return_once(move |_, _| {
1027                    Ok(TotalBalance {
1028                        balance: 7,
1029                        num_coins: 9,
1030                    })
1031                });
1032            let coin_read_api = CoinReadApi::new_for_tests(Arc::new(mock_state), None);
1033            let response = coin_read_api.get_balance(owner, None).await;
1034
1035            assert!(response.is_ok());
1036            let result = response.unwrap();
1037            assert_eq!(
1038                result,
1039                Balance {
1040                    coin_type: gas_coin.coin_type,
1041                    coin_object_count: 9,
1042                    total_balance: 7,
1043                }
1044            );
1045        }
1046
1047        #[tokio::test]
1048        async fn test_with_coin_type() {
1049            let owner = get_test_owner();
1050            let coin = get_test_coin(None, CoinType::Usdc);
1051            let coin_clone = coin.clone();
1052            let mut mock_state = MockStateRead::new();
1053            mock_state
1054                .expect_get_balance()
1055                .with(
1056                    predicate::eq(owner),
1057                    predicate::eq(get_test_coin_type_tag(coin_clone.coin_type)),
1058                )
1059                .return_once(move |_, _| {
1060                    Ok(TotalBalance {
1061                        balance: 10,
1062                        num_coins: 11,
1063                    })
1064                });
1065            let coin_read_api = CoinReadApi::new_for_tests(Arc::new(mock_state), None);
1066            let response = coin_read_api
1067                .get_balance(owner, Some(coin.coin_type.clone()))
1068                .await;
1069
1070            assert!(response.is_ok());
1071            let result = response.unwrap();
1072            assert_eq!(
1073                result,
1074                Balance {
1075                    coin_type: coin.coin_type,
1076                    coin_object_count: 11,
1077                    total_balance: 10,
1078                }
1079            );
1080        }
1081
1082        // Expected error scenarios
1083        #[tokio::test]
1084        async fn test_invalid_coin_type() {
1085            let owner = get_test_owner();
1086            let coin_type = "0x2::invalid::struct::tag";
1087            let mock_state = MockStateRead::new();
1088            let coin_read_api = CoinReadApi::new_for_tests(Arc::new(mock_state), None);
1089            let response = coin_read_api
1090                .get_balance(owner, Some(coin_type.to_string()))
1091                .await;
1092
1093            assert!(response.is_err());
1094            let error_result = response.unwrap_err();
1095            let expected = expect!["-32602"];
1096            expected.assert_eq(&error_result.code().to_string());
1097            let expected = expect![
1098                "Invalid struct type: 0x2::invalid::struct::tag. Got error: Expected end of token stream. Got: ::"
1099            ];
1100            expected.assert_eq(error_result.message());
1101        }
1102
1103        // Unexpected error scenarios
1104        #[tokio::test]
1105        async fn test_get_balance_index_store_not_available() {
1106            let owner = get_test_owner();
1107            let coin_type = get_test_coin_type(get_test_package_id());
1108            let mut mock_state = MockStateRead::new();
1109            mock_state.expect_get_balance().returning(move |_, _| {
1110                Err(StateReadError::Client(
1111                    IotaError::IndexStoreNotAvailable.into(),
1112                ))
1113            });
1114            let coin_read_api = CoinReadApi::new_for_tests(Arc::new(mock_state), None);
1115            let response = coin_read_api
1116                .get_balance(owner, Some(coin_type.to_string()))
1117                .await;
1118
1119            assert!(response.is_err());
1120            let error_result = response.unwrap_err();
1121            assert_eq!(
1122                error_result.code(),
1123                jsonrpsee::types::error::INVALID_PARAMS_CODE
1124            );
1125            let expected = expect!["Index store not available on this Fullnode."];
1126            expected.assert_eq(error_result.message());
1127        }
1128
1129        #[tokio::test]
1130        async fn test_get_balance_execution_error() {
1131            // Validate that we handle and return an error message when we encounter an
1132            // unexpected error
1133            let owner = get_test_owner();
1134            let coin_type = get_test_coin_type(get_test_package_id());
1135            let mut mock_state = MockStateRead::new();
1136            mock_state.expect_get_balance().returning(move |_, _| {
1137                Err(IotaError::Execution("mock db error".to_string()).into())
1138            });
1139            let coin_read_api = CoinReadApi::new_for_tests(Arc::new(mock_state), None);
1140            let response = coin_read_api
1141                .get_balance(owner, Some(coin_type.to_string()))
1142                .await;
1143
1144            assert!(response.is_err());
1145            let error_result = response.unwrap_err();
1146
1147            assert_eq!(
1148                error_result.code(),
1149                jsonrpsee::types::error::INTERNAL_ERROR_CODE
1150            );
1151            let expected = expect!["Error executing mock db error"];
1152            expected.assert_eq(error_result.message());
1153        }
1154    }
1155
1156    mod get_all_balances_tests {
1157        use super::{super::*, *};
1158
1159        // Success scenarios
1160        #[tokio::test]
1161        async fn test_success_scenario() {
1162            let owner = get_test_owner();
1163            let gas_coin = get_test_coin(None, CoinType::Gas);
1164            let gas_coin_type_tag = get_test_coin_type_tag(gas_coin.coin_type.clone());
1165            let usdc_coin = get_test_coin(None, CoinType::Usdc);
1166            let usdc_coin_type_tag = get_test_coin_type_tag(usdc_coin.coin_type.clone());
1167            let mut mock_state = MockStateRead::new();
1168            mock_state
1169                .expect_get_all_balance()
1170                .with(predicate::eq(owner))
1171                .return_once(move |_| {
1172                    let mut hash_map = HashMap::new();
1173                    hash_map.insert(
1174                        gas_coin_type_tag,
1175                        TotalBalance {
1176                            balance: 7,
1177                            num_coins: 9,
1178                        },
1179                    );
1180                    hash_map.insert(
1181                        usdc_coin_type_tag,
1182                        TotalBalance {
1183                            balance: 10,
1184                            num_coins: 11,
1185                        },
1186                    );
1187                    Ok(Arc::new(hash_map))
1188                });
1189            let coin_read_api = CoinReadApi::new_for_tests(Arc::new(mock_state), None);
1190            let response = coin_read_api.get_all_balances(owner).await;
1191
1192            assert!(response.is_ok());
1193            let expected_result = vec![
1194                Balance {
1195                    coin_type: gas_coin.coin_type,
1196                    coin_object_count: 9,
1197                    total_balance: 7,
1198                },
1199                Balance {
1200                    coin_type: usdc_coin.coin_type,
1201                    coin_object_count: 11,
1202                    total_balance: 10,
1203                },
1204            ];
1205            // This is because the underlying result is a hashmap, so order is not
1206            // guaranteed
1207            let mut result = response.unwrap();
1208            for item in expected_result {
1209                if let Some(pos) = result.iter().position(|i| *i == item) {
1210                    result.remove(pos);
1211                } else {
1212                    panic!("{:?} not found in result", item);
1213                }
1214            }
1215            assert!(result.is_empty());
1216        }
1217
1218        // Unexpected error scenarios
1219        #[tokio::test]
1220        async fn test_index_store_not_available() {
1221            let owner = get_test_owner();
1222            let mut mock_state = MockStateRead::new();
1223            mock_state.expect_get_all_balance().returning(move |_| {
1224                Err(StateReadError::Client(
1225                    IotaError::IndexStoreNotAvailable.into(),
1226                ))
1227            });
1228            let coin_read_api = CoinReadApi::new_for_tests(Arc::new(mock_state), None);
1229            let response = coin_read_api.get_all_balances(owner).await;
1230
1231            assert!(response.is_err());
1232            let error_result = response.unwrap_err();
1233            assert_eq!(
1234                error_result.code(),
1235                jsonrpsee::types::error::INVALID_PARAMS_CODE
1236            );
1237            let expected = expect!["Index store not available on this Fullnode."];
1238            expected.assert_eq(error_result.message());
1239        }
1240    }
1241
1242    mod get_coin_metadata_tests {
1243        use iota_types::id::UID;
1244        use mockall::predicate;
1245
1246        use super::{super::*, *};
1247
1248        // Success scenarios
1249        #[tokio::test]
1250        async fn test_valid_coin_metadata_object() {
1251            let package_id = get_test_package_id();
1252            let coin_name = get_test_coin_type(package_id);
1253            let input_coin_struct = parse_iota_struct_tag(&coin_name).expect("should not fail");
1254            let coin_metadata_struct = CoinMetadata::type_(input_coin_struct.clone());
1255            let coin_metadata = CoinMetadata {
1256                id: UID::new(get_test_package_id()),
1257                decimals: 2,
1258                name: "test_coin".to_string(),
1259                symbol: "TEST".to_string(),
1260                description: "test coin".to_string(),
1261                icon_url: Some("unit.test.io".to_string()),
1262            };
1263            let coin_metadata_object =
1264                Object::coin_metadata_for_testing(input_coin_struct.clone(), coin_metadata);
1265            let metadata = IotaCoinMetadata::try_from(coin_metadata_object.clone()).unwrap();
1266            let mut mock_internal = MockCoinReadInternal::new();
1267            // return TreasuryCap instead of CoinMetadata to set up test
1268            mock_internal
1269                .expect_find_package_object()
1270                .with(predicate::always(), predicate::eq(coin_metadata_struct))
1271                .return_once(move |object_id, _| {
1272                    if object_id == &package_id {
1273                        Ok(coin_metadata_object)
1274                    } else {
1275                        panic!("should not be called with any other object id")
1276                    }
1277                });
1278
1279            let coin_read_api = CoinReadApi {
1280                internal: Box::new(mock_internal),
1281                unlocks_store: MainnetUnlocksStore::new().unwrap(),
1282            };
1283
1284            let response = coin_read_api.get_coin_metadata(coin_name.clone()).await;
1285            assert!(response.is_ok());
1286            let result = response.unwrap().unwrap();
1287            assert_eq!(result, metadata);
1288        }
1289
1290        #[tokio::test]
1291        async fn test_object_not_found() {
1292            let transaction_digest = TransactionDigest::from([0; 32]);
1293            let transaction_effects = TransactionEffects::default();
1294
1295            let mut mock_state = MockStateRead::new();
1296            mock_state
1297                .expect_find_publish_txn_digest()
1298                .return_once(move |_| Ok(transaction_digest));
1299            mock_state
1300                .expect_get_executed_transaction_and_effects()
1301                .return_once(move |_, _| Ok((create_fake_transaction(), transaction_effects)));
1302
1303            let coin_read_api = CoinReadApi::new_for_tests(Arc::new(mock_state), None);
1304            let response = coin_read_api
1305                .get_coin_metadata("0x2::iota::IOTA".to_string())
1306                .await;
1307
1308            assert!(response.is_ok());
1309            let result = response.unwrap();
1310            assert_eq!(result, None);
1311        }
1312
1313        #[tokio::test]
1314        async fn test_find_package_object_not_iota_coin_metadata() {
1315            let package_id = get_test_package_id();
1316            let coin_name = get_test_coin_type(package_id);
1317            let input_coin_struct = parse_iota_struct_tag(&coin_name).expect("should not fail");
1318            let coin_metadata_struct = CoinMetadata::type_(input_coin_struct.clone());
1319            let treasury_cap = TreasuryCap {
1320                id: UID::new(get_test_package_id()),
1321                total_supply: Supply { value: 420 },
1322            };
1323            let treasury_cap_object =
1324                Object::treasury_cap_for_testing(input_coin_struct.clone(), treasury_cap);
1325            let mut mock_internal = MockCoinReadInternal::new();
1326            // return TreasuryCap instead of CoinMetadata to set up test
1327            mock_internal
1328                .expect_find_package_object()
1329                .with(predicate::always(), predicate::eq(coin_metadata_struct))
1330                .returning(move |object_id, _| {
1331                    if object_id == &package_id {
1332                        Ok(treasury_cap_object.clone())
1333                    } else {
1334                        panic!("should not be called with any other object id")
1335                    }
1336                });
1337
1338            let coin_read_api = CoinReadApi {
1339                internal: Box::new(mock_internal),
1340                unlocks_store: MainnetUnlocksStore::new().unwrap(),
1341            };
1342
1343            let response = coin_read_api.get_coin_metadata(coin_name.clone()).await;
1344            assert!(response.is_ok());
1345            let result = response.unwrap();
1346            assert!(result.is_none());
1347        }
1348    }
1349
1350    mod get_total_supply_tests {
1351        use iota_types::{
1352            collection_types::VecMap,
1353            gas_coin::IotaTreasuryCap,
1354            id::UID,
1355            iota_system_state::{
1356                IotaSystemState,
1357                iota_system_state_inner_v1::{StorageFundV1, SystemParametersV1},
1358                iota_system_state_inner_v2::{IotaSystemStateV2, ValidatorSetV2},
1359            },
1360        };
1361        use mockall::predicate;
1362
1363        use super::{super::*, *};
1364
1365        #[tokio::test]
1366        async fn test_success_response_for_gas_coin() {
1367            let coin_type = "0x2::iota::IOTA";
1368
1369            let mut mock_state = MockStateRead::new();
1370            mock_state.expect_get_system_state().returning(move || {
1371                let mut state = default_system_state();
1372                state.iota_treasury_cap.inner.total_supply.value = 42;
1373
1374                Ok(IotaSystemState::V2(state))
1375            });
1376
1377            let coin_read_api = CoinReadApi::new_for_tests(Arc::new(mock_state), None);
1378
1379            let response = coin_read_api.get_total_supply(coin_type.to_string()).await;
1380
1381            let supply = response.unwrap();
1382            assert_eq!(supply.value, 42);
1383        }
1384
1385        #[tokio::test]
1386        async fn test_success_response_for_other_coin() {
1387            let package_id = get_test_package_id();
1388            let (coin_name, _, treasury_cap_struct, _, treasury_cap_object) =
1389                get_test_treasury_cap_peripherals(package_id);
1390            let mut mock_internal = MockCoinReadInternal::new();
1391            mock_internal
1392                .expect_find_package_object()
1393                .with(predicate::always(), predicate::eq(treasury_cap_struct))
1394                .returning(move |object_id, _| {
1395                    if object_id == &package_id {
1396                        Ok(treasury_cap_object.clone())
1397                    } else {
1398                        panic!("should not be called with any other object id")
1399                    }
1400                });
1401            let coin_read_api = CoinReadApi {
1402                internal: Box::new(mock_internal),
1403                unlocks_store: MainnetUnlocksStore::new().unwrap(),
1404            };
1405
1406            let response = coin_read_api.get_total_supply(coin_name.clone()).await;
1407
1408            assert!(response.is_ok());
1409            let result = response.unwrap();
1410            let expected = expect!["420"];
1411            expected.assert_eq(&result.value.to_string());
1412        }
1413
1414        #[tokio::test]
1415        async fn test_object_not_found() {
1416            let package_id = get_test_package_id();
1417            let (coin_name, _, _, _, _) = get_test_treasury_cap_peripherals(package_id);
1418            let transaction_digest = TransactionDigest::from([0; 32]);
1419            let transaction_effects = TransactionEffects::default();
1420
1421            let mut mock_state = MockStateRead::new();
1422            mock_state
1423                .expect_find_publish_txn_digest()
1424                .return_once(move |_| Ok(transaction_digest));
1425            mock_state
1426                .expect_get_executed_transaction_and_effects()
1427                .return_once(move |_, _| Ok((create_fake_transaction(), transaction_effects)));
1428
1429            let coin_read_api = CoinReadApi::new_for_tests(Arc::new(mock_state), None);
1430            let response = coin_read_api.get_total_supply(coin_name.clone()).await;
1431
1432            assert!(response.is_err());
1433            let error_result = response.unwrap_err();
1434            let expected = expect!["-32602"];
1435            expected.assert_eq(&error_result.code().to_string());
1436            let expected = expect![
1437                "Cannot find object [0x2::coin::TreasuryCap<0xf::test_coin::TEST_COIN>] from [0x000000000000000000000000000000000000000000000000000000000000000f] package event."
1438            ];
1439            expected.assert_eq(error_result.message());
1440        }
1441
1442        #[tokio::test]
1443        async fn test_find_package_object_not_treasury_cap() {
1444            let package_id = get_test_package_id();
1445            let (coin_name, input_coin_struct, treasury_cap_struct, _, _) =
1446                get_test_treasury_cap_peripherals(package_id);
1447            let coin_metadata = CoinMetadata {
1448                id: UID::new(get_test_package_id()),
1449                decimals: 2,
1450                name: "test_coin".to_string(),
1451                symbol: "TEST".to_string(),
1452                description: "test coin".to_string(),
1453                icon_url: None,
1454            };
1455            let coin_metadata_object =
1456                Object::coin_metadata_for_testing(input_coin_struct.clone(), coin_metadata);
1457            let mut mock_internal = MockCoinReadInternal::new();
1458            mock_internal
1459                .expect_find_package_object()
1460                .with(predicate::always(), predicate::eq(treasury_cap_struct))
1461                .returning(move |object_id, _| {
1462                    if object_id == &package_id {
1463                        Ok(coin_metadata_object.clone())
1464                    } else {
1465                        panic!("should not be called with any other object id")
1466                    }
1467                });
1468
1469            let coin_read_api = CoinReadApi {
1470                internal: Box::new(mock_internal),
1471                unlocks_store: MainnetUnlocksStore::new().unwrap(),
1472            };
1473
1474            let response = coin_read_api.get_total_supply(coin_name.clone()).await;
1475            let error_result = response.unwrap_err();
1476            assert_eq!(
1477                error_result.code(),
1478                jsonrpsee::types::error::CALL_EXECUTION_FAILED_CODE
1479            );
1480            let expected = expect![
1481                "Failure deserializing object in the requested format: \"Unable to deserialize TreasuryCap object: remaining input\""
1482            ];
1483            expected.assert_eq(error_result.message());
1484        }
1485
1486        fn default_system_state() -> IotaSystemStateV2 {
1487            IotaSystemStateV2 {
1488                epoch: Default::default(),
1489                protocol_version: Default::default(),
1490                system_state_version: Default::default(),
1491                iota_treasury_cap: IotaTreasuryCap {
1492                    inner: TreasuryCap {
1493                        id: UID::new(ObjectID::random()),
1494                        total_supply: Supply {
1495                            value: Default::default(),
1496                        },
1497                    },
1498                },
1499                validators: ValidatorSetV2 {
1500                    total_stake: Default::default(),
1501                    active_validators: Default::default(),
1502                    committee_members: Default::default(),
1503                    pending_active_validators: Default::default(),
1504                    pending_removals: Default::default(),
1505                    staking_pool_mappings: Default::default(),
1506                    inactive_validators: Default::default(),
1507                    validator_candidates: Default::default(),
1508                    at_risk_validators: VecMap {
1509                        contents: Default::default(),
1510                    },
1511                    extra_fields: Default::default(),
1512                },
1513                storage_fund: StorageFundV1 {
1514                    total_object_storage_rebates: iota_types::balance::Balance::new(
1515                        Default::default(),
1516                    ),
1517                    non_refundable_balance: iota_types::balance::Balance::new(Default::default()),
1518                },
1519                parameters: SystemParametersV1 {
1520                    epoch_duration_ms: Default::default(),
1521                    min_validator_count: Default::default(),
1522                    max_validator_count: Default::default(),
1523                    min_validator_joining_stake: Default::default(),
1524                    validator_low_stake_threshold: Default::default(),
1525                    validator_very_low_stake_threshold: Default::default(),
1526                    validator_low_stake_grace_period: Default::default(),
1527                    extra_fields: Default::default(),
1528                },
1529                iota_system_admin_cap: Default::default(),
1530                reference_gas_price: Default::default(),
1531                validator_report_records: VecMap {
1532                    contents: Default::default(),
1533                },
1534                safe_mode: Default::default(),
1535                safe_mode_storage_charges: iota_types::balance::Balance::new(Default::default()),
1536                safe_mode_computation_charges: iota_types::balance::Balance::new(Default::default()),
1537                safe_mode_computation_charges_burned: Default::default(),
1538                safe_mode_storage_rebates: Default::default(),
1539                safe_mode_non_refundable_storage_fee: Default::default(),
1540                epoch_start_timestamp_ms: Default::default(),
1541                extra_fields: Default::default(),
1542            }
1543        }
1544    }
1545}