sui_indexer_alt_jsonrpc/api/
coin.rs1use std::collections::HashMap;
5use std::str::FromStr;
6
7use anyhow::Context as _;
8use futures::future;
9use jsonrpsee::core::RpcResult;
10use jsonrpsee::proc_macros::rpc;
11use move_core_types::language_storage::StructTag;
12use move_core_types::language_storage::TypeTag;
13use sui_indexer_alt_reader::consistent_reader::proto::Balance as ProtoBalance;
14use sui_indexer_alt_reader::consistent_reader::proto::owner::OwnerKind;
15use sui_json_rpc_types::Balance;
16use sui_json_rpc_types::Coin;
17use sui_json_rpc_types::Page as PageResponse;
18use sui_json_rpc_types::SuiCoinMetadata;
19use sui_open_rpc::Module;
20use sui_open_rpc_macros::open_rpc;
21use sui_types::SUI_FRAMEWORK_ADDRESS;
22use sui_types::base_types::ObjectID;
23use sui_types::base_types::SuiAddress;
24use sui_types::coin::COIN_METADATA_STRUCT_NAME;
25use sui_types::coin::COIN_MODULE_NAME;
26use sui_types::coin::COIN_STRUCT_NAME;
27use sui_types::coin::CoinMetadata;
28use sui_types::coin_registry::Currency;
29use sui_types::gas_coin::GAS;
30use sui_types::object::Object;
31
32use crate::api::rpc_module::RpcModule;
33use crate::context::Context;
34use crate::data::load_live;
35use crate::error::InternalContext;
36use crate::error::RpcError;
37use crate::error::invalid_params;
38use crate::paginate::BcsCursor;
39use crate::paginate::Cursor as _;
40use crate::paginate::Page;
41
42#[open_rpc(namespace = "suix", tag = "Coin API")]
43#[rpc(server, namespace = "suix")]
44trait CoinsApi {
45 #[method(name = "getCoins")]
48 async fn get_coins(
49 &self,
50 owner: SuiAddress,
52 coin_type: Option<String>,
54 cursor: Option<String>,
56 limit: Option<usize>,
58 ) -> RpcResult<PageResponse<Coin, String>>;
59
60 #[method(name = "getCoinMetadata")]
64 async fn get_coin_metadata(
65 &self,
66 coin_type: String,
68 ) -> RpcResult<Option<SuiCoinMetadata>>;
69
70 #[method(name = "getAllBalances")]
72 async fn get_all_balances(
73 &self,
74 owner: SuiAddress,
76 ) -> RpcResult<Vec<Balance>>;
77
78 #[method(name = "getBalance")]
81 async fn get_balance(
82 &self,
83 owner: SuiAddress,
85 coin_type: Option<String>,
87 ) -> RpcResult<Balance>;
88}
89
90pub(crate) struct Coins(pub Context);
91
92#[derive(thiserror::Error, Debug)]
93pub(crate) enum Error {
94 #[error("Pagination issue: {0}")]
95 Pagination(#[from] crate::paginate::Error),
96
97 #[error("Failed to parse type {0:?}: {1}")]
98 BadType(String, anyhow::Error),
99}
100
101type Cursor = BcsCursor<Vec<u8>>;
102
103#[async_trait::async_trait]
104impl CoinsApiServer for Coins {
105 async fn get_coins(
106 &self,
107 owner: SuiAddress,
108 coin_type: Option<String>,
109 cursor: Option<String>,
110 limit: Option<usize>,
111 ) -> RpcResult<PageResponse<Coin, String>> {
112 let inner = if let Some(coin_type) = coin_type {
113 TypeTag::from_str(&coin_type)
114 .map_err(|e| invalid_params(Error::BadType(coin_type, e)))?
115 } else {
116 GAS::type_tag()
117 };
118
119 let object_type = StructTag {
120 address: SUI_FRAMEWORK_ADDRESS,
121 module: COIN_MODULE_NAME.to_owned(),
122 name: COIN_STRUCT_NAME.to_owned(),
123 type_params: vec![inner],
124 };
125
126 let Self(ctx) = self;
127 let config = &ctx.config().coins;
128
129 let page: Page<Cursor> = Page::from_params::<Error>(
130 config.default_page_size,
131 config.max_page_size,
132 cursor,
133 limit,
134 None,
135 )?;
136
137 let consistent_reader = ctx.consistent_reader();
138
139 let results = consistent_reader
142 .list_owned_objects(
143 None, OwnerKind::Address,
145 Some(owner.to_string()),
146 Some(object_type.to_canonical_string(true)),
147 Some(page.limit as u32),
148 page.cursor.as_ref().map(|c| c.0.clone()),
149 None,
150 true,
151 )
152 .await
153 .context("Failed to list owned coin objects")
154 .map_err(RpcError::<Error>::from)?;
155
156 let coin_ids: Vec<_> = results
157 .results
158 .iter()
159 .map(|obj_ref| obj_ref.value.0)
160 .collect();
161
162 let next_cursor = results
163 .results
164 .last()
165 .map(|edge| BcsCursor(edge.token.clone()).encode())
166 .transpose()
167 .context("Failed to encode cursor")
168 .map_err(RpcError::<Error>::from)?;
169
170 let coin_futures = coin_ids.iter().map(|id| coin_response(ctx, *id));
171
172 let coins = future::join_all(coin_futures)
173 .await
174 .into_iter()
175 .zip(coin_ids)
176 .map(|(r, id)| r.with_internal_context(|| format!("Failed to get object {id}")))
177 .collect::<Result<Vec<_>, _>>()?;
178
179 Ok(PageResponse {
180 data: coins,
181 next_cursor,
182 has_next_page: results.has_next_page,
183 })
184 }
185
186 async fn get_coin_metadata(&self, coin_type: String) -> RpcResult<Option<SuiCoinMetadata>> {
187 let Self(ctx) = self;
188
189 if let Some(currency) = coin_registry_response(ctx, &coin_type)
190 .await
191 .with_internal_context(|| format!("Failed to fetch Currency for {coin_type:?}"))?
192 {
193 return Ok(Some(currency));
194 }
195
196 if let Some(metadata) = coin_metadata_response(ctx, &coin_type)
197 .await
198 .with_internal_context(|| format!("Failed to fetch CoinMetadata for {coin_type:?}"))?
199 {
200 return Ok(Some(metadata));
201 }
202
203 Ok(None)
204 }
205
206 async fn get_all_balances(&self, owner: SuiAddress) -> RpcResult<Vec<Balance>> {
207 let Self(ctx) = self;
208 let consistent_reader = ctx.consistent_reader();
209 let config = &ctx.config().coins;
210
211 let mut all_balances = Vec::new();
212 let mut after_token: Option<Vec<u8>> = None;
213
214 loop {
215 let page = consistent_reader
216 .list_balances(
217 None,
218 owner.to_string(),
219 Some(config.max_page_size as u32),
220 after_token.clone(),
221 None,
222 true,
223 )
224 .await
225 .context("Failed to get all balances")
226 .map_err(RpcError::<Error>::from)?;
227
228 for edge in &page.results {
229 all_balances.push(try_from_proto(edge.value.clone())?);
230 }
231
232 if page.has_next_page {
233 after_token = page.results.last().map(|edge| edge.token.clone());
234 } else {
235 break;
236 }
237 }
238
239 Ok(all_balances)
240 }
241
242 async fn get_balance(
243 &self,
244 owner: SuiAddress,
245 coin_type: Option<String>,
246 ) -> RpcResult<Balance> {
247 let Self(ctx) = self;
248 let consistent_reader = ctx.consistent_reader();
249
250 let inner_coin_type = if let Some(coin_type) = coin_type {
251 TypeTag::from_str(&coin_type)
252 .map_err(|e| invalid_params(Error::BadType(coin_type, e)))?
253 } else {
254 GAS::type_tag()
255 };
256
257 let response = consistent_reader
258 .get_balance(
259 None,
260 owner.to_string(),
261 inner_coin_type.to_canonical_string(true),
262 )
263 .await
264 .context("Failed to get balance")
265 .map_err(RpcError::<Error>::from)?;
266
267 Ok(try_from_proto(response)?)
268 }
269}
270
271impl RpcModule for Coins {
272 fn schema(&self) -> Module {
273 CoinsApiOpenRpc::module_doc()
274 }
275
276 fn into_impl(self) -> jsonrpsee::RpcModule<Self> {
277 self.into_rpc()
278 }
279}
280
281fn try_from_proto(proto: ProtoBalance) -> Result<Balance, RpcError<Error>> {
282 let coin_type: TypeTag = proto
283 .coin_type
284 .context("coin type missing")?
285 .parse()
286 .context("invalid coin type")?;
287 Ok(Balance {
288 coin_type: coin_type.to_canonical_string(true),
289 total_balance: proto.total_balance.unwrap_or(0) as u128,
290 coin_object_count: 1,
293 locked_balance: HashMap::new(),
294 funds_in_address_balance: proto.address_balance.unwrap_or(0) as u128,
295 })
296}
297
298async fn coin_response(ctx: &Context, id: ObjectID) -> Result<Coin, RpcError<Error>> {
299 let (object, coin_type, balance) = object_with_coin_data(ctx, id).await?;
300
301 let coin_object_id = object.id();
302 let digest = object.digest();
303 let version = object.version();
304 let previous_transaction = object.as_inner().previous_transaction;
305
306 Ok(Coin {
307 coin_type,
308 coin_object_id,
309 version,
310 digest,
311 balance,
312 previous_transaction,
313 })
314}
315
316async fn coin_registry_response(
317 ctx: &Context,
318 coin_type: &str,
319) -> Result<Option<SuiCoinMetadata>, RpcError<Error>> {
320 let coin_type = TypeTag::from_str(coin_type)
321 .map_err(|e| invalid_params(Error::BadType(coin_type.to_owned(), e)))?;
322
323 let currency_id = Currency::derive_object_id(coin_type)
324 .context("Failed to derive object id for coin registry Currency")?;
325
326 let Some(object) = load_live(ctx, currency_id)
327 .await
328 .context("Failed to load Currency object")?
329 else {
330 return Ok(None);
331 };
332
333 let Some(move_object) = object.data.try_as_move() else {
334 return Ok(None);
335 };
336
337 let currency: Currency =
338 bcs::from_bytes(move_object.contents()).context("Failed to parse Currency object")?;
339
340 Ok(Some(currency.into()))
341}
342
343async fn coin_metadata_response(
345 ctx: &Context,
346 coin_type: &str,
347) -> Result<Option<SuiCoinMetadata>, RpcError<Error>> {
348 let inner = TypeTag::from_str(coin_type)
349 .map_err(|e| invalid_params(Error::BadType(coin_type.to_owned(), e)))?;
350
351 let object_type = StructTag {
352 address: SUI_FRAMEWORK_ADDRESS,
353 module: COIN_MODULE_NAME.to_owned(),
354 name: COIN_METADATA_STRUCT_NAME.to_owned(),
355 type_params: vec![inner],
356 };
357
358 let Some(obj_ref) = ctx
359 .consistent_reader()
360 .list_objects_by_type(
361 None,
362 object_type.to_canonical_string(true),
363 Some(1),
364 None,
365 None,
366 false,
367 )
368 .await
369 .context("Failed to load object reference for CoinMetadata")?
370 .results
371 .into_iter()
372 .next()
373 else {
374 return Ok(None);
375 };
376
377 let id = obj_ref.value.0;
378
379 let Some(object) = load_live(ctx, id)
380 .await
381 .context("Failed to load latest version of CoinMetadata")?
382 else {
383 return Ok(None);
384 };
385
386 let Some(move_object) = object.data.try_as_move() else {
387 return Ok(None);
388 };
389
390 let coin_metadata: CoinMetadata =
391 bcs::from_bytes(move_object.contents()).context("Failed to parse Currency object")?;
392
393 Ok(Some(coin_metadata.into()))
394}
395
396async fn object_with_coin_data(
397 ctx: &Context,
398 id: ObjectID,
399) -> Result<(Object, String, u64), RpcError<Error>> {
400 let object = load_live(ctx, id)
401 .await?
402 .with_context(|| format!("Failed to load latest object {id}"))?;
403
404 let coin = object
405 .as_coin_maybe()
406 .context("Object is expected to be a coin")?;
407 let coin_type = object
408 .coin_type_maybe()
409 .context("Object is expected to have a coin type")?
410 .to_canonical_string(true);
411 Ok((object, coin_type, coin.balance.value()))
412}