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, OwnedObjectCursor, 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 apis::common,
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: Address,
48 coin_type: Option<String>,
49 cursor: Option<OwnedObjectCursor>,
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 = cursor
64 .map(common::resolve_object_id_cursor)
65 .transpose()?
66 .unwrap_or(ObjectId::ZERO);
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
75 .last()
76 .map(|o| OwnedObjectCursor::from_object_id(o.coin_object_id));
77 Ok(Page {
78 data: results,
79 next_cursor,
80 has_next_page,
81 oldest_available_checkpoint: None,
82 })
83 }
84
85 async fn get_all_coins(
86 &self,
87 owner: Address,
88 cursor: Option<OwnedObjectCursor>,
89 limit: Option<usize>,
90 ) -> RpcResult<CoinPage> {
91 let limit = cap_page_limit(limit);
92 if limit == 0 {
93 return Ok(CoinPage::empty());
94 }
95
96 let cursor = cursor
99 .map(common::resolve_object_id_cursor)
100 .transpose()?
101 .unwrap_or(ObjectId::ZERO);
102 let mut results = self
103 .inner
104 .get_owned_coins_in_blocking_task(owner, None, cursor, limit + 1)
105 .await?;
106
107 let has_next_page = results.len() > limit;
108 results.truncate(limit);
109 let next_cursor = results
110 .last()
111 .map(|o| OwnedObjectCursor::from_object_id(o.coin_object_id));
112 Ok(Page {
113 data: results,
114 next_cursor,
115 has_next_page,
116 oldest_available_checkpoint: None,
117 })
118 }
119
120 async fn get_balance(&self, owner: Address, coin_type: Option<String>) -> RpcResult<Balance> {
121 let coin_type =
123 parse_to_type_tag(coin_type)?.to_canonical_string(true);
124
125 let mut results = self
126 .inner
127 .get_coin_balances_in_blocking_task(owner, Some(coin_type.clone()))
128 .await?;
129 if results.is_empty() {
130 return Ok(Balance::zero(coin_type));
131 }
132 Ok(results.swap_remove(0))
133 }
134
135 async fn get_all_balances(&self, owner: Address) -> RpcResult<Vec<Balance>> {
136 self.inner
137 .get_coin_balances_in_blocking_task(owner, None)
138 .await
139 .map_err(Into::into)
140 }
141
142 async fn get_coin_metadata(&self, coin_type: String) -> RpcResult<Option<IotaCoinMetadata>> {
143 let coin_struct = parse_to_struct_tag(&coin_type)?;
144 self.inner
145 .get_coin_metadata_in_blocking_task(coin_struct)
146 .await
147 .map_err(Into::into)
148 }
149
150 async fn get_total_supply(&self, coin_type: String) -> RpcResult<IotaSupply> {
151 let coin_struct = parse_to_struct_tag(&coin_type)?;
152 if coin_struct.is_gas() {
153 Ok(Supply {
154 value: self
155 .inner
156 .spawn_blocking(|this| this.get_latest_iota_system_state())
157 .await?
158 .iota_total_supply(),
159 }
160 .into())
161 } else {
162 self.inner
163 .get_total_supply_in_blocking_task(coin_struct)
164 .await
165 .map(Into::into)
166 .map_err(Into::into)
167 }
168 }
169
170 async fn get_circulating_supply(&self) -> RpcResult<IotaCirculatingSupply> {
171 let latest_cp = self
172 .inner
173 .spawn_blocking(|this| this.get_latest_checkpoint())
174 .await?;
175 let cp_timestamp_ms = latest_cp.timestamp_ms;
176
177 let total_supply = self
178 .inner
179 .spawn_blocking(|this| this.get_latest_iota_system_state())
180 .await?
181 .iota_total_supply();
182
183 let date_time =
184 DateTime::from_timestamp_millis(cp_timestamp_ms.try_into().map_err(|_| {
185 InvalidArgument(format!("failed to convert timestamp: {cp_timestamp_ms}"))
186 })?)
187 .ok_or(DateTimeParsing(format!(
188 "failed to parse timestamp: {cp_timestamp_ms}"
189 )))?;
190
191 let chain = self
192 .inner
193 .get_chain_identifier_in_blocking_task()
194 .await?
195 .chain();
196
197 let locked_supply = match chain {
198 Chain::Mainnet => self.unlocks_store.still_locked_tokens(date_time),
199 _ => 0,
200 };
201
202 let circulating_supply = total_supply - locked_supply;
203 let circulating_supply_percentage = circulating_supply as f64 / total_supply as f64;
204
205 Ok(IotaCirculatingSupply {
206 value: circulating_supply,
207 circulating_supply_percentage,
208 at_checkpoint: latest_cp.sequence_number,
209 })
210 }
211}
212
213impl IotaRpcModule for CoinReadApi {
214 fn rpc(self) -> RpcModule<Self> {
215 self.into_rpc()
216 }
217
218 fn rpc_doc_module() -> Module {
219 iota_json_rpc_api::CoinReadApiOpenRpc::module_doc()
220 }
221}