iota_indexer/apis/
coin_api.rs1use anyhow::Result;
6use async_trait::async_trait;
7use chrono::DateTime;
8use iota_json_rpc::{
9 IotaRpcModule,
10 coin_api::{parse_to_struct_tag, parse_to_type_tag},
11};
12use iota_json_rpc_api::{CoinReadApiServer, cap_page_limit};
13use iota_json_rpc_types::{Balance, CoinPage, IotaCirculatingSupply, IotaCoinMetadata, Page};
14use iota_mainnet_unlocks::MainnetUnlocksStore;
15use iota_open_rpc::Module;
16use iota_protocol_config::Chain;
17use iota_types::{
18 balance::Supply,
19 base_types::{IotaAddress, ObjectID},
20 gas_coin::GAS,
21};
22use jsonrpsee::{RpcModule, core::RpcResult};
23
24use crate::{
25 errors::IndexerError::{DateTimeParsing, InvalidArgument},
26 indexer_reader::IndexerReader,
27 types::IotaSystemStateSummaryView,
28};
29
30pub(crate) struct CoinReadApi {
31 inner: IndexerReader,
32 unlocks_store: MainnetUnlocksStore,
33}
34
35impl CoinReadApi {
36 pub fn new(inner: IndexerReader) -> Result<Self> {
37 Ok(Self {
38 inner,
39 unlocks_store: MainnetUnlocksStore::new()?,
40 })
41 }
42}
43
44#[async_trait]
45impl CoinReadApiServer for CoinReadApi {
46 async fn get_coins(
47 &self,
48 owner: IotaAddress,
49 coin_type: Option<String>,
50 cursor: Option<ObjectID>,
51 limit: Option<usize>,
52 ) -> RpcResult<CoinPage> {
53 let limit = cap_page_limit(limit);
54 if limit == 0 {
55 return Ok(CoinPage::empty());
56 }
57
58 let coin_type =
60 parse_to_type_tag(coin_type)?.to_canonical_string(true);
61
62 let cursor = match cursor {
63 Some(c) => c,
64 None => ObjectID::ZERO,
67 };
68 let mut results = self
69 .inner
70 .get_owned_coins_in_blocking_task(owner, Some(coin_type), cursor, limit + 1)
71 .await?;
72
73 let has_next_page = results.len() > limit;
74 results.truncate(limit);
75 let next_cursor = results.last().map(|o| o.coin_object_id);
76 Ok(Page {
77 data: results,
78 next_cursor,
79 has_next_page,
80 })
81 }
82
83 async fn get_all_coins(
84 &self,
85 owner: IotaAddress,
86 cursor: Option<ObjectID>,
87 limit: Option<usize>,
88 ) -> RpcResult<CoinPage> {
89 let limit = cap_page_limit(limit);
90 if limit == 0 {
91 return Ok(CoinPage::empty());
92 }
93
94 let cursor = match cursor {
95 Some(c) => c,
96 None => ObjectID::ZERO,
99 };
100 let mut results = self
101 .inner
102 .get_owned_coins_in_blocking_task(owner, None, cursor, limit + 1)
103 .await?;
104
105 let has_next_page = results.len() > limit;
106 results.truncate(limit);
107 let next_cursor = results.last().map(|o| o.coin_object_id);
108 Ok(Page {
109 data: results,
110 next_cursor,
111 has_next_page,
112 })
113 }
114
115 async fn get_balance(
116 &self,
117 owner: IotaAddress,
118 coin_type: Option<String>,
119 ) -> RpcResult<Balance> {
120 let coin_type =
122 parse_to_type_tag(coin_type)?.to_canonical_string(true);
123
124 let mut results = self
125 .inner
126 .get_coin_balances_in_blocking_task(owner, Some(coin_type.clone()))
127 .await?;
128 if results.is_empty() {
129 return Ok(Balance::zero(coin_type));
130 }
131 Ok(results.swap_remove(0))
132 }
133
134 async fn get_all_balances(&self, owner: IotaAddress) -> RpcResult<Vec<Balance>> {
135 self.inner
136 .get_coin_balances_in_blocking_task(owner, None)
137 .await
138 .map_err(Into::into)
139 }
140
141 async fn get_coin_metadata(&self, coin_type: String) -> RpcResult<Option<IotaCoinMetadata>> {
142 let coin_struct = parse_to_struct_tag(&coin_type)?;
143 self.inner
144 .get_coin_metadata_in_blocking_task(coin_struct)
145 .await
146 .map_err(Into::into)
147 }
148
149 async fn get_total_supply(&self, coin_type: String) -> RpcResult<Supply> {
150 let coin_struct = parse_to_struct_tag(&coin_type)?;
151 if GAS::is_gas(&coin_struct) {
152 Ok(Supply {
153 value: self
154 .inner
155 .spawn_blocking(|this| this.get_latest_iota_system_state())
156 .await?
157 .iota_total_supply(),
158 })
159 } else {
160 self.inner
161 .get_total_supply_in_blocking_task(coin_struct)
162 .await
163 .map_err(Into::into)
164 }
165 }
166
167 async fn get_circulating_supply(&self) -> RpcResult<IotaCirculatingSupply> {
168 let latest_cp = self
169 .inner
170 .spawn_blocking(|this| this.get_latest_checkpoint())
171 .await?;
172 let cp_timestamp_ms = latest_cp.timestamp_ms;
173
174 let total_supply = self
175 .inner
176 .spawn_blocking(|this| this.get_latest_iota_system_state())
177 .await?
178 .iota_total_supply();
179
180 let date_time =
181 DateTime::from_timestamp_millis(cp_timestamp_ms.try_into().map_err(|_| {
182 InvalidArgument(format!("failed to convert timestamp: {cp_timestamp_ms}"))
183 })?)
184 .ok_or(DateTimeParsing(format!(
185 "failed to parse timestamp: {cp_timestamp_ms}"
186 )))?;
187
188 let chain = self
189 .inner
190 .get_chain_identifier_in_blocking_task()
191 .await?
192 .chain();
193
194 let locked_supply = match chain {
195 Chain::Mainnet => self.unlocks_store.still_locked_tokens(date_time),
196 _ => 0,
197 };
198
199 let circulating_supply = total_supply - locked_supply;
200 let circulating_supply_percentage = circulating_supply as f64 / total_supply as f64;
201
202 Ok(IotaCirculatingSupply {
203 value: circulating_supply,
204 circulating_supply_percentage,
205 at_checkpoint: latest_cp.sequence_number,
206 })
207 }
208}
209
210impl IotaRpcModule for CoinReadApi {
211 fn rpc(self) -> RpcModule<Self> {
212 self.into_rpc()
213 }
214
215 fn rpc_doc_module() -> Module {
216 iota_json_rpc_api::CoinReadApiOpenRpc::module_doc()
217 }
218}