1use 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 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 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 None => (coin_type_tag.to_string(), ObjectID::ZERO),
109 };
110
111 self.internal
112 .get_coins_iterator(
113 owner, cursor, limit, true, )
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 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 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, )
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#[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 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 #[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 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 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 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 #[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 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 #[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 #[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 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 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}