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::{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        // 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<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}