sui_indexer/apis/
coin_api.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::indexer_reader::IndexerReader;
5use async_trait::async_trait;
6use jsonrpsee::RpcModule;
7use jsonrpsee::core::RpcResult;
8use sui_json_rpc::SuiRpcModule;
9use sui_json_rpc::coin_api::{parse_to_struct_tag, parse_to_type_tag};
10use sui_json_rpc::error::SuiRpcInputError;
11use sui_json_rpc_api::{CoinReadApiServer, cap_page_limit};
12use sui_json_rpc_types::{Balance, CoinPage, Page, SuiCoinMetadata};
13use sui_open_rpc::Module;
14use sui_types::balance::Supply;
15use sui_types::base_types::{ObjectID, SuiAddress};
16use sui_types::gas_coin::{GAS, TOTAL_SUPPLY_MIST};
17
18pub(crate) struct CoinReadApi {
19    inner: IndexerReader,
20}
21
22impl CoinReadApi {
23    pub fn new(inner: IndexerReader) -> Self {
24        Self { inner }
25    }
26}
27
28#[async_trait]
29impl CoinReadApiServer for CoinReadApi {
30    async fn get_coins(
31        &self,
32        owner: SuiAddress,
33        coin_type: Option<String>,
34        cursor: Option<String>,
35        limit: Option<usize>,
36    ) -> RpcResult<CoinPage> {
37        let limit = cap_page_limit(limit);
38        if limit == 0 {
39            return Ok(CoinPage::empty());
40        }
41
42        // Normalize coin type tag and default to Gas
43        let coin_type =
44            parse_to_type_tag(coin_type)?.to_canonical_string(/* with_prefix */ true);
45
46        let cursor = match cursor {
47            Some(c) => c
48                .parse()
49                .map_err(|e| SuiRpcInputError::GenericInvalid(format!("invalid cursor: {e}")))?,
50            // If cursor is not specified, we need to start from the beginning of the coin type, which is the minimal possible ObjectID.
51            None => ObjectID::ZERO,
52        };
53        let mut results = self
54            .inner
55            .get_owned_coins(owner, Some(coin_type), cursor, limit + 1)
56            .await?;
57
58        let has_next_page = results.len() > limit;
59        results.truncate(limit);
60        let next_cursor = results.last().map(|o| o.coin_object_id.to_string());
61        Ok(Page {
62            data: results,
63            next_cursor,
64            has_next_page,
65        })
66    }
67
68    async fn get_all_coins(
69        &self,
70        owner: SuiAddress,
71        cursor: Option<String>,
72        limit: Option<usize>,
73    ) -> RpcResult<CoinPage> {
74        let limit = cap_page_limit(limit);
75        if limit == 0 {
76            return Ok(CoinPage::empty());
77        }
78
79        let cursor = match cursor {
80            Some(c) => c
81                .parse()
82                .map_err(|e| SuiRpcInputError::GenericInvalid(format!("invalid cursor: {e}")))?,
83            // If cursor is not specified, we need to start from the beginning of the coin type, which is the minimal possible ObjectID.
84            None => ObjectID::ZERO,
85        };
86        let mut results = self
87            .inner
88            .get_owned_coins(owner, None, cursor, limit + 1)
89            .await?;
90
91        let has_next_page = results.len() > limit;
92        results.truncate(limit);
93        let next_cursor = results.last().map(|o| o.coin_object_id.to_string());
94        Ok(Page {
95            data: results,
96            next_cursor,
97            has_next_page,
98        })
99    }
100
101    async fn get_balance(
102        &self,
103        owner: SuiAddress,
104        coin_type: Option<String>,
105    ) -> RpcResult<Balance> {
106        // Normalize coin type tag and default to Gas
107        let coin_type =
108            parse_to_type_tag(coin_type)?.to_canonical_string(/* with_prefix */ true);
109
110        let mut results = self
111            .inner
112            .get_coin_balances(owner, Some(coin_type.clone()))
113            .await?;
114        if results.is_empty() {
115            return Ok(Balance::zero(coin_type));
116        }
117        Ok(results.swap_remove(0))
118    }
119
120    async fn get_all_balances(&self, owner: SuiAddress) -> RpcResult<Vec<Balance>> {
121        self.inner
122            .get_coin_balances(owner, None)
123            .await
124            .map_err(Into::into)
125    }
126
127    async fn get_coin_metadata(&self, coin_type: String) -> RpcResult<Option<SuiCoinMetadata>> {
128        let coin_struct = parse_to_struct_tag(&coin_type)?;
129        self.inner
130            .get_coin_metadata(coin_struct)
131            .await
132            .map_err(Into::into)
133    }
134
135    async fn get_total_supply(&self, coin_type: String) -> RpcResult<Supply> {
136        let coin_struct = parse_to_struct_tag(&coin_type)?;
137        if GAS::is_gas(&coin_struct) {
138            Ok(Supply {
139                value: TOTAL_SUPPLY_MIST,
140            })
141        } else {
142            self.inner
143                .get_total_supply(coin_struct)
144                .await
145                .map_err(Into::into)
146        }
147    }
148}
149
150impl SuiRpcModule for CoinReadApi {
151    fn rpc(self) -> RpcModule<Self> {
152        self.into_rpc()
153    }
154
155    fn rpc_doc_module() -> Module {
156        sui_json_rpc_api::CoinReadApiOpenRpc::module_doc()
157    }
158}