Skip to main content

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, 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        // 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        // This store pages by object id alone. Without a cursor, start from
62        // the minimal possible ObjectId.
63        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        // This store pages by object id alone. Without a cursor, start from
97        // the minimal possible ObjectId.
98        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        // Normalize coin type tag and default to Gas
122        let coin_type =
123            parse_to_type_tag(coin_type)?.to_canonical_string(/* with_prefix */ 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}