sui_indexer/apis/
coin_api.rs1use 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 let coin_type =
44 parse_to_type_tag(coin_type)?.to_canonical_string(true);
45
46 let cursor = match cursor {
47 Some(c) => c
48 .parse()
49 .map_err(|e| SuiRpcInputError::GenericInvalid(format!("invalid cursor: {e}")))?,
50 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 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 let coin_type =
108 parse_to_type_tag(coin_type)?.to_canonical_string(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}