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