1use crate::connection::ScanConnection;
5use crate::consistency::{build_objects_query, View};
6use crate::data::{Db, QueryExecutor};
7use crate::error::Error;
8use crate::filter;
9use crate::raw_query::RawQuery;
10
11use super::available_range::AvailableRange;
12use super::balance::{self, Balance};
13use super::base64::Base64;
14use super::big_int::BigInt;
15use super::cursor::{Page, Target};
16use super::display::DisplayEntry;
17use super::dynamic_field::{DynamicField, DynamicFieldName};
18use super::move_object::{MoveObject, MoveObjectImpl};
19use super::move_value::MoveValue;
20use super::object::{self, Object, ObjectFilter, ObjectImpl, ObjectOwner, ObjectStatus};
21use super::owner::OwnerImpl;
22use super::stake::StakedSui;
23use super::sui_address::SuiAddress;
24use super::suins_registration::{DomainFormat, SuinsRegistration};
25use super::transaction_block::{self, TransactionBlock, TransactionBlockFilter};
26use super::type_filter::ExactTypeFilter;
27use super::uint53::UInt53;
28use async_graphql::*;
29
30use async_graphql::connection::{Connection, CursorType, Edge};
31use diesel_async::scoped_futures::ScopedFutureExt;
32use sui_indexer::models::objects::StoredHistoryObject;
33use sui_indexer::types::OwnerType;
34use sui_types::coin::Coin as NativeCoin;
35use sui_types::TypeTag;
36
37#[derive(Clone)]
38pub(crate) struct Coin {
39 pub super_: MoveObject,
41
42 pub native: NativeCoin,
44}
45
46pub(crate) enum CoinDowncastError {
47 NotACoin,
48 Bcs(bcs::Error),
49}
50
51#[Object]
53impl Coin {
54 pub(crate) async fn address(&self) -> SuiAddress {
55 OwnerImpl::from(&self.super_.super_).address().await
56 }
57
58 pub(crate) async fn objects(
60 &self,
61 ctx: &Context<'_>,
62 first: Option<u64>,
63 after: Option<object::Cursor>,
64 last: Option<u64>,
65 before: Option<object::Cursor>,
66 filter: Option<ObjectFilter>,
67 ) -> Result<Connection<String, MoveObject>> {
68 OwnerImpl::from(&self.super_.super_)
69 .objects(ctx, first, after, last, before, filter)
70 .await
71 }
72
73 pub(crate) async fn balance(
76 &self,
77 ctx: &Context<'_>,
78 type_: Option<ExactTypeFilter>,
79 ) -> Result<Option<Balance>> {
80 OwnerImpl::from(&self.super_.super_)
81 .balance(ctx, type_)
82 .await
83 }
84
85 pub(crate) async fn balances(
87 &self,
88 ctx: &Context<'_>,
89 first: Option<u64>,
90 after: Option<balance::Cursor>,
91 last: Option<u64>,
92 before: Option<balance::Cursor>,
93 ) -> Result<Connection<String, Balance>> {
94 OwnerImpl::from(&self.super_.super_)
95 .balances(ctx, first, after, last, before)
96 .await
97 }
98
99 pub(crate) async fn coins(
103 &self,
104 ctx: &Context<'_>,
105 first: Option<u64>,
106 after: Option<object::Cursor>,
107 last: Option<u64>,
108 before: Option<object::Cursor>,
109 type_: Option<ExactTypeFilter>,
110 ) -> Result<Connection<String, Coin>> {
111 OwnerImpl::from(&self.super_.super_)
112 .coins(ctx, first, after, last, before, type_)
113 .await
114 }
115
116 pub(crate) async fn staked_suis(
118 &self,
119 ctx: &Context<'_>,
120 first: Option<u64>,
121 after: Option<object::Cursor>,
122 last: Option<u64>,
123 before: Option<object::Cursor>,
124 ) -> Result<Connection<String, StakedSui>> {
125 OwnerImpl::from(&self.super_.super_)
126 .staked_suis(ctx, first, after, last, before)
127 .await
128 }
129
130 pub(crate) async fn default_suins_name(
132 &self,
133 ctx: &Context<'_>,
134 format: Option<DomainFormat>,
135 ) -> Result<Option<String>> {
136 OwnerImpl::from(&self.super_.super_)
137 .default_suins_name(ctx, format)
138 .await
139 }
140
141 pub(crate) async fn suins_registrations(
144 &self,
145 ctx: &Context<'_>,
146 first: Option<u64>,
147 after: Option<object::Cursor>,
148 last: Option<u64>,
149 before: Option<object::Cursor>,
150 ) -> Result<Connection<String, SuinsRegistration>> {
151 OwnerImpl::from(&self.super_.super_)
152 .suins_registrations(ctx, first, after, last, before)
153 .await
154 }
155
156 pub(crate) async fn version(&self) -> UInt53 {
157 ObjectImpl(&self.super_.super_).version().await
158 }
159
160 pub(crate) async fn status(&self) -> ObjectStatus {
168 ObjectImpl(&self.super_.super_).status().await
169 }
170
171 pub(crate) async fn digest(&self) -> Option<String> {
173 ObjectImpl(&self.super_.super_).digest().await
174 }
175
176 pub(crate) async fn owner(&self) -> Option<ObjectOwner> {
178 ObjectImpl(&self.super_.super_).owner().await
179 }
180
181 pub(crate) async fn previous_transaction_block(
183 &self,
184 ctx: &Context<'_>,
185 ) -> Result<Option<TransactionBlock>> {
186 ObjectImpl(&self.super_.super_)
187 .previous_transaction_block(ctx)
188 .await
189 }
190
191 pub(crate) async fn storage_rebate(&self) -> Option<BigInt> {
194 ObjectImpl(&self.super_.super_).storage_rebate().await
195 }
196
197 pub(crate) async fn received_transaction_blocks(
218 &self,
219 ctx: &Context<'_>,
220 first: Option<u64>,
221 after: Option<transaction_block::Cursor>,
222 last: Option<u64>,
223 before: Option<transaction_block::Cursor>,
224 filter: Option<TransactionBlockFilter>,
225 scan_limit: Option<u64>,
226 ) -> Result<ScanConnection<String, TransactionBlock>> {
227 ObjectImpl(&self.super_.super_)
228 .received_transaction_blocks(ctx, first, after, last, before, filter, scan_limit)
229 .await
230 }
231
232 pub(crate) async fn bcs(&self) -> Result<Option<Base64>> {
234 ObjectImpl(&self.super_.super_).bcs().await
235 }
236
237 pub(crate) async fn contents(&self) -> Option<MoveValue> {
241 MoveObjectImpl(&self.super_).contents().await
242 }
243
244 pub(crate) async fn has_public_transfer(&self, ctx: &Context<'_>) -> Result<bool> {
248 MoveObjectImpl(&self.super_).has_public_transfer(ctx).await
249 }
250
251 pub(crate) async fn display(&self, ctx: &Context<'_>) -> Result<Option<Vec<DisplayEntry>>> {
255 ObjectImpl(&self.super_.super_).display(ctx).await
256 }
257
258 pub(crate) async fn dynamic_field(
265 &self,
266 ctx: &Context<'_>,
267 name: DynamicFieldName,
268 ) -> Result<Option<DynamicField>> {
269 OwnerImpl::from(&self.super_.super_)
270 .dynamic_field(ctx, name, Some(self.super_.root_version()))
271 .await
272 }
273
274 pub(crate) async fn dynamic_object_field(
282 &self,
283 ctx: &Context<'_>,
284 name: DynamicFieldName,
285 ) -> Result<Option<DynamicField>> {
286 OwnerImpl::from(&self.super_.super_)
287 .dynamic_object_field(ctx, name, Some(self.super_.root_version()))
288 .await
289 }
290
291 pub(crate) async fn dynamic_fields(
296 &self,
297 ctx: &Context<'_>,
298 first: Option<u64>,
299 after: Option<object::Cursor>,
300 last: Option<u64>,
301 before: Option<object::Cursor>,
302 ) -> Result<Connection<String, DynamicField>> {
303 OwnerImpl::from(&self.super_.super_)
304 .dynamic_fields(
305 ctx,
306 first,
307 after,
308 last,
309 before,
310 Some(self.super_.root_version()),
311 )
312 .await
313 }
314
315 async fn coin_balance(&self) -> Option<BigInt> {
317 Some(BigInt::from(self.native.balance.value()))
318 }
319}
320
321impl Coin {
322 pub(crate) async fn paginate(
325 db: &Db,
326 page: Page<object::Cursor>,
327 coin_type: TypeTag,
328 owner: Option<SuiAddress>,
329 checkpoint_viewed_at: u64,
330 ) -> Result<Connection<String, Coin>, Error> {
331 let cursor_viewed_at = page.validate_cursor_consistency()?;
335 let checkpoint_viewed_at = cursor_viewed_at.unwrap_or(checkpoint_viewed_at);
336
337 let Some((prev, next, results)) = db
338 .execute_repeatable(move |conn| {
339 async move {
340 let Some(range) = AvailableRange::result(conn, checkpoint_viewed_at).await?
341 else {
342 return Ok::<_, diesel::result::Error>(None);
343 };
344
345 Ok(Some(
346 page.paginate_raw_query::<StoredHistoryObject>(
347 conn,
348 checkpoint_viewed_at,
349 coins_query(coin_type, owner, range, &page),
350 )
351 .await?,
352 ))
353 }
354 .scope_boxed()
355 })
356 .await?
357 else {
358 return Err(Error::Client(
359 "Requested data is outside the available range".to_string(),
360 ));
361 };
362
363 let mut conn: Connection<String, Coin> = Connection::new(prev, next);
364
365 for stored in results {
366 let cursor = stored.cursor(checkpoint_viewed_at).encode_cursor();
369 let object =
370 Object::try_from_stored_history_object(stored, checkpoint_viewed_at, None)?;
371
372 let move_ = MoveObject::try_from(&object).map_err(|_| {
373 Error::Internal(format!(
374 "Failed to deserialize as Move object: {}",
375 object.address
376 ))
377 })?;
378
379 let coin = Coin::try_from(&move_).map_err(|_| {
380 Error::Internal(format!("Faild to deserialize as Coin: {}", object.address))
381 })?;
382
383 conn.edges.push(Edge::new(cursor, coin));
384 }
385
386 Ok(conn)
387 }
388}
389
390impl TryFrom<&MoveObject> for Coin {
391 type Error = CoinDowncastError;
392
393 fn try_from(move_object: &MoveObject) -> Result<Self, Self::Error> {
394 if !move_object.native.is_coin() {
395 return Err(CoinDowncastError::NotACoin);
396 }
397
398 Ok(Self {
399 super_: move_object.clone(),
400 native: bcs::from_bytes(move_object.native.contents())
401 .map_err(CoinDowncastError::Bcs)?,
402 })
403 }
404}
405
406fn coins_query(
410 coin_type: TypeTag,
411 owner: Option<SuiAddress>,
412 range: AvailableRange,
413 page: &Page<object::Cursor>,
414) -> RawQuery {
415 build_objects_query(
416 View::Consistent,
417 range,
418 page,
419 move |query| apply_filter(query, &coin_type, owner),
420 move |newer| newer,
421 )
422}
423
424fn apply_filter(mut query: RawQuery, coin_type: &TypeTag, owner: Option<SuiAddress>) -> RawQuery {
425 if let Some(owner) = owner {
426 query = filter!(
427 query,
428 format!(
429 "owner_id = '\\x{}'::bytea AND owner_type = {}",
430 hex::encode(owner.into_vec()),
431 OwnerType::Address as i16
432 )
433 );
434 }
435
436 query = filter!(
437 query,
438 "coin_type IS NOT NULL AND coin_type = {} AND object_status = 0",
439 coin_type.to_canonical_display(true)
440 );
441
442 query
443}