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    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        // Normalize coin type tag and default to Gas
58        let coin_type =
59            parse_to_type_tag(coin_type)?.to_canonical_string(/* with_prefix */ true);
60
61        let cursor = match cursor {
62            Some(c) => c,
63            // If cursor is not specified, we need to start from the beginning of the coin type,
64            // which is the minimal possible ObjectID.
65            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            // If cursor is not specified, we need to start from the beginning of the coin type,
96            // which is the minimal possible ObjectID.
97            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        // Normalize coin type tag and default to Gas
120        let coin_type =
121            parse_to_type_tag(coin_type)?.to_canonical_string(/* with_prefix */ 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}