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