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, 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    errors::IndexerError::{DateTimeParsing, InvalidArgument},
25    read::IndexerReader,
26};
27
28pub(crate) struct CoinReadApi {
29    inner: IndexerReader,
30    unlocks_store: MainnetUnlocksStore,
31}
32
33impl CoinReadApi {
34    pub fn new(inner: IndexerReader) -> Result<Self> {
35        Ok(Self {
36            inner,
37            unlocks_store: MainnetUnlocksStore::new()?,
38        })
39    }
40}
41
42#[async_trait]
43impl CoinReadApiServer for CoinReadApi {
44    async fn get_coins(
45        &self,
46        owner: Address,
47        coin_type: Option<String>,
48        cursor: Option<ObjectId>,
49        limit: Option<usize>,
50    ) -> RpcResult<CoinPage> {
51        let limit = cap_page_limit(limit);
52        if limit == 0 {
53            return Ok(CoinPage::empty());
54        }
55
56        // Normalize coin type tag and default to Gas
57        let coin_type =
58            parse_to_type_tag(coin_type)?.to_canonical_string(/* with_prefix */ true);
59
60        let cursor = match cursor {
61            Some(c) => c,
62            // If cursor is not specified, we need to start from the beginning of the coin type,
63            // which is the minimal possible ObjectId.
64            None => ObjectId::ZERO,
65        };
66        let mut results = self
67            .inner
68            .get_owned_coins_in_blocking_task(owner, Some(coin_type), cursor, limit + 1)
69            .await?;
70
71        let has_next_page = results.len() > limit;
72        results.truncate(limit);
73        let next_cursor = results.last().map(|o| o.coin_object_id);
74        Ok(Page {
75            data: results,
76            next_cursor,
77            has_next_page,
78        })
79    }
80
81    async fn get_all_coins(
82        &self,
83        owner: Address,
84        cursor: Option<ObjectId>,
85        limit: Option<usize>,
86    ) -> RpcResult<CoinPage> {
87        let limit = cap_page_limit(limit);
88        if limit == 0 {
89            return Ok(CoinPage::empty());
90        }
91
92        let cursor = match cursor {
93            Some(c) => c,
94            // If cursor is not specified, we need to start from the beginning of the coin type,
95            // which is the minimal possible ObjectId.
96            None => ObjectId::ZERO,
97        };
98        let mut results = self
99            .inner
100            .get_owned_coins_in_blocking_task(owner, None, cursor, limit + 1)
101            .await?;
102
103        let has_next_page = results.len() > limit;
104        results.truncate(limit);
105        let next_cursor = results.last().map(|o| o.coin_object_id);
106        Ok(Page {
107            data: results,
108            next_cursor,
109            has_next_page,
110        })
111    }
112
113    async fn get_balance(&self, owner: Address, coin_type: Option<String>) -> RpcResult<Balance> {
114        // Normalize coin type tag and default to Gas
115        let coin_type =
116            parse_to_type_tag(coin_type)?.to_canonical_string(/* with_prefix */ true);
117
118        let mut results = self
119            .inner
120            .get_coin_balances_in_blocking_task(owner, Some(coin_type.clone()))
121            .await?;
122        if results.is_empty() {
123            return Ok(Balance::zero(coin_type));
124        }
125        Ok(results.swap_remove(0))
126    }
127
128    async fn get_all_balances(&self, owner: Address) -> RpcResult<Vec<Balance>> {
129        self.inner
130            .get_coin_balances_in_blocking_task(owner, None)
131            .await
132            .map_err(Into::into)
133    }
134
135    async fn get_coin_metadata(&self, coin_type: String) -> RpcResult<Option<IotaCoinMetadata>> {
136        let coin_struct = parse_to_struct_tag(&coin_type)?;
137        self.inner
138            .get_coin_metadata_in_blocking_task(coin_struct)
139            .await
140            .map_err(Into::into)
141    }
142
143    async fn get_total_supply(&self, coin_type: String) -> RpcResult<IotaSupply> {
144        let coin_struct = parse_to_struct_tag(&coin_type)?;
145        if coin_struct.is_gas() {
146            Ok(Supply {
147                value: self
148                    .inner
149                    .spawn_blocking(|this| this.get_latest_iota_system_state())
150                    .await?
151                    .iota_total_supply(),
152            }
153            .into())
154        } else {
155            self.inner
156                .get_total_supply_in_blocking_task(coin_struct)
157                .await
158                .map(Into::into)
159                .map_err(Into::into)
160        }
161    }
162
163    async fn get_circulating_supply(&self) -> RpcResult<IotaCirculatingSupply> {
164        let latest_cp = self
165            .inner
166            .spawn_blocking(|this| this.get_latest_checkpoint())
167            .await?;
168        let cp_timestamp_ms = latest_cp.timestamp_ms;
169
170        let total_supply = self
171            .inner
172            .spawn_blocking(|this| this.get_latest_iota_system_state())
173            .await?
174            .iota_total_supply();
175
176        let date_time =
177            DateTime::from_timestamp_millis(cp_timestamp_ms.try_into().map_err(|_| {
178                InvalidArgument(format!("failed to convert timestamp: {cp_timestamp_ms}"))
179            })?)
180            .ok_or(DateTimeParsing(format!(
181                "failed to parse timestamp: {cp_timestamp_ms}"
182            )))?;
183
184        let chain = self
185            .inner
186            .get_chain_identifier_in_blocking_task()
187            .await?
188            .chain();
189
190        let locked_supply = match chain {
191            Chain::Mainnet => self.unlocks_store.still_locked_tokens(date_time),
192            _ => 0,
193        };
194
195        let circulating_supply = total_supply - locked_supply;
196        let circulating_supply_percentage = circulating_supply as f64 / total_supply as f64;
197
198        Ok(IotaCirculatingSupply {
199            value: circulating_supply,
200            circulating_supply_percentage,
201            at_checkpoint: latest_cp.sequence_number,
202        })
203    }
204}
205
206impl IotaRpcModule for CoinReadApi {
207    fn rpc(self) -> RpcModule<Self> {
208        self.into_rpc()
209    }
210
211    fn rpc_doc_module() -> Module {
212        iota_json_rpc_api::CoinReadApiOpenRpc::module_doc()
213    }
214}