iota_indexer/apis/
coin_api.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use 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_types::{
20    balance::Supply,
21    base_types::{IotaAddress, ObjectID},
22};
23use jsonrpsee::{RpcModule, core::RpcResult};
24
25use crate::{
26    errors::IndexerError::{DateTimeParsing, InvalidArgument},
27    read::IndexerReader,
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        // Normalize coin type tag and default to Gas
59        let coin_type =
60            parse_to_type_tag(coin_type)?.to_canonical_string(/* with_prefix */ true);
61
62        let cursor = match cursor {
63            Some(c) => c,
64            // If cursor is not specified, we need to start from the beginning of the coin type,
65            // which is the minimal possible ObjectID.
66            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            // If cursor is not specified, we need to start from the beginning of the coin type,
97            // which is the minimal possible ObjectID.
98            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        // Normalize coin type tag and default to Gas
121        let coin_type =
122            parse_to_type_tag(coin_type)?.to_canonical_string(/* with_prefix */ 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<IotaSupply> {
150        let coin_struct = parse_to_struct_tag(&coin_type)?;
151        if coin_struct.is_gas() {
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            .into())
160        } else {
161            self.inner
162                .get_total_supply_in_blocking_task(coin_struct)
163                .await
164                .map(Into::into)
165                .map_err(Into::into)
166        }
167    }
168
169    async fn get_circulating_supply(&self) -> RpcResult<IotaCirculatingSupply> {
170        let latest_cp = self
171            .inner
172            .spawn_blocking(|this| this.get_latest_checkpoint())
173            .await?;
174        let cp_timestamp_ms = latest_cp.timestamp_ms;
175
176        let total_supply = self
177            .inner
178            .spawn_blocking(|this| this.get_latest_iota_system_state())
179            .await?
180            .iota_total_supply();
181
182        let date_time =
183            DateTime::from_timestamp_millis(cp_timestamp_ms.try_into().map_err(|_| {
184                InvalidArgument(format!("failed to convert timestamp: {cp_timestamp_ms}"))
185            })?)
186            .ok_or(DateTimeParsing(format!(
187                "failed to parse timestamp: {cp_timestamp_ms}"
188            )))?;
189
190        let chain = self
191            .inner
192            .get_chain_identifier_in_blocking_task()
193            .await?
194            .chain();
195
196        let locked_supply = match chain {
197            Chain::Mainnet => self.unlocks_store.still_locked_tokens(date_time),
198            _ => 0,
199        };
200
201        let circulating_supply = total_supply - locked_supply;
202        let circulating_supply_percentage = circulating_supply as f64 / total_supply as f64;
203
204        Ok(IotaCirculatingSupply {
205            value: circulating_supply,
206            circulating_supply_percentage,
207            at_checkpoint: latest_cp.sequence_number,
208        })
209    }
210}
211
212impl IotaRpcModule for CoinReadApi {
213    fn rpc(self) -> RpcModule<Self> {
214        self.into_rpc()
215    }
216
217    fn rpc_doc_module() -> Module {
218        iota_json_rpc_api::CoinReadApiOpenRpc::module_doc()
219    }
220}