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.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 #[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 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 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 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 #[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 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 #[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 #[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 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 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}