1use async_graphql::{
6 connection::{Connection, CursorType, Edge},
7 *,
8};
9use iota_indexer::{models::objects::StoredHistoryObject, types::OwnerType};
10use iota_types::{TypeTag, coin::Coin as NativeCoin};
11
12use crate::{
13 connection::ScanConnection,
14 consistency::{View, build_objects_query},
15 data::{Db, QueryExecutor},
16 error::Error,
17 filter,
18 raw_query::RawQuery,
19 types::{
20 available_range::AvailableRange,
21 balance::{self, Balance},
22 base64::Base64,
23 big_int::BigInt,
24 cursor::{Page, Target},
25 display::DisplayEntry,
26 dynamic_field::{DynamicField, DynamicFieldName},
27 iota_address::IotaAddress,
28 iota_names_registration::{NameFormat, NameRegistration},
29 move_object::{MoveObject, MoveObjectImpl},
30 move_value::MoveValue,
31 object::{self, Object, ObjectFilter, ObjectImpl, ObjectOwner, ObjectStatus},
32 owner::OwnerImpl,
33 stake::StakedIota,
34 transaction_block::{self, TransactionBlock, TransactionBlockFilter},
35 type_filter::ExactTypeFilter,
36 uint53::UInt53,
37 },
38};
39
40#[derive(Clone)]
41pub(crate) struct Coin {
42 pub super_: MoveObject,
44
45 pub native: NativeCoin,
48}
49
50pub(crate) enum CoinDowncastError {
51 NotACoin,
52 Bcs(bcs::Error),
53}
54
55#[Object]
57impl Coin {
58 pub(crate) async fn address(&self) -> IotaAddress {
59 OwnerImpl::from(&self.super_.super_).address().await
60 }
61
62 pub(crate) async fn objects(
64 &self,
65 ctx: &Context<'_>,
66 first: Option<u64>,
67 after: Option<object::Cursor>,
68 last: Option<u64>,
69 before: Option<object::Cursor>,
70 filter: Option<ObjectFilter>,
71 ) -> Result<Connection<String, MoveObject>> {
72 OwnerImpl::from(&self.super_.super_)
73 .objects(ctx, first, after, last, before, filter)
74 .await
75 }
76
77 pub(crate) async fn balance(
80 &self,
81 ctx: &Context<'_>,
82 type_: Option<ExactTypeFilter>,
83 ) -> Result<Option<Balance>> {
84 OwnerImpl::from(&self.super_.super_)
85 .balance(ctx, type_)
86 .await
87 }
88
89 pub(crate) async fn balances(
91 &self,
92 ctx: &Context<'_>,
93 first: Option<u64>,
94 after: Option<balance::Cursor>,
95 last: Option<u64>,
96 before: Option<balance::Cursor>,
97 ) -> Result<Connection<String, Balance>> {
98 OwnerImpl::from(&self.super_.super_)
99 .balances(ctx, first, after, last, before)
100 .await
101 }
102
103 pub(crate) async fn coins(
108 &self,
109 ctx: &Context<'_>,
110 first: Option<u64>,
111 after: Option<object::Cursor>,
112 last: Option<u64>,
113 before: Option<object::Cursor>,
114 type_: Option<ExactTypeFilter>,
115 ) -> Result<Connection<String, Coin>> {
116 OwnerImpl::from(&self.super_.super_)
117 .coins(ctx, first, after, last, before, type_)
118 .await
119 }
120
121 pub(crate) async fn staked_iotas(
123 &self,
124 ctx: &Context<'_>,
125 first: Option<u64>,
126 after: Option<object::Cursor>,
127 last: Option<u64>,
128 before: Option<object::Cursor>,
129 ) -> Result<Connection<String, StakedIota>> {
130 OwnerImpl::from(&self.super_.super_)
131 .staked_iotas(ctx, first, after, last, before)
132 .await
133 }
134
135 pub(crate) async fn iota_names_default_name(
138 &self,
139 ctx: &Context<'_>,
140 format: Option<NameFormat>,
141 ) -> Result<Option<String>> {
142 OwnerImpl::from(&self.super_.super_)
143 .iota_names_default_name(ctx, format)
144 .await
145 }
146
147 pub(crate) async fn iota_names_registrations(
150 &self,
151 ctx: &Context<'_>,
152 first: Option<u64>,
153 after: Option<object::Cursor>,
154 last: Option<u64>,
155 before: Option<object::Cursor>,
156 ) -> Result<Connection<String, NameRegistration>> {
157 OwnerImpl::from(&self.super_.super_)
158 .iota_names_registrations(ctx, first, after, last, before)
159 .await
160 }
161
162 pub(crate) async fn version(&self) -> UInt53 {
163 ObjectImpl(&self.super_.super_).version().await
164 }
165
166 pub(crate) async fn status(&self) -> ObjectStatus {
175 ObjectImpl(&self.super_.super_).status().await
176 }
177
178 pub(crate) async fn digest(&self) -> Option<String> {
181 ObjectImpl(&self.super_.super_).digest().await
182 }
183
184 pub(crate) async fn owner(&self, ctx: &Context<'_>) -> Option<ObjectOwner> {
186 ObjectImpl(&self.super_.super_).owner(ctx).await
187 }
188
189 pub(crate) async fn previous_transaction_block(
191 &self,
192 ctx: &Context<'_>,
193 ) -> Result<Option<TransactionBlock>> {
194 ObjectImpl(&self.super_.super_)
195 .previous_transaction_block(ctx)
196 .await
197 }
198
199 pub(crate) async fn storage_rebate(&self) -> Option<BigInt> {
203 ObjectImpl(&self.super_.super_).storage_rebate().await
204 }
205
206 pub(crate) async fn received_transaction_blocks(
232 &self,
233 ctx: &Context<'_>,
234 first: Option<u64>,
235 after: Option<transaction_block::Cursor>,
236 last: Option<u64>,
237 before: Option<transaction_block::Cursor>,
238 filter: Option<TransactionBlockFilter>,
239 scan_limit: Option<u64>,
240 ) -> Result<ScanConnection<String, TransactionBlock>> {
241 ObjectImpl(&self.super_.super_)
242 .received_transaction_blocks(ctx, first, after, last, before, filter, scan_limit)
243 .await
244 }
245
246 pub(crate) async fn bcs(&self) -> Result<Option<Base64>> {
248 ObjectImpl(&self.super_.super_).bcs().await
249 }
250
251 pub(crate) async fn contents(&self) -> Option<MoveValue> {
255 MoveObjectImpl(&self.super_).contents().await
256 }
257
258 pub(crate) async fn display(&self, ctx: &Context<'_>) -> Result<Option<Vec<DisplayEntry>>> {
262 ObjectImpl(&self.super_.super_).display(ctx).await
263 }
264
265 pub(crate) async fn dynamic_field(
272 &self,
273 ctx: &Context<'_>,
274 name: DynamicFieldName,
275 ) -> Result<Option<DynamicField>> {
276 OwnerImpl::from(&self.super_.super_)
277 .dynamic_field(ctx, name, Some(self.super_.root_version()))
278 .await
279 }
280
281 pub(crate) async fn dynamic_object_field(
290 &self,
291 ctx: &Context<'_>,
292 name: DynamicFieldName,
293 ) -> Result<Option<DynamicField>> {
294 OwnerImpl::from(&self.super_.super_)
295 .dynamic_object_field(ctx, name, Some(self.super_.root_version()))
296 .await
297 }
298
299 pub(crate) async fn dynamic_fields(
304 &self,
305 ctx: &Context<'_>,
306 first: Option<u64>,
307 after: Option<object::Cursor>,
308 last: Option<u64>,
309 before: Option<object::Cursor>,
310 ) -> Result<Connection<String, DynamicField>> {
311 OwnerImpl::from(&self.super_.super_)
312 .dynamic_fields(
313 ctx,
314 first,
315 after,
316 last,
317 before,
318 Some(self.super_.root_version()),
319 )
320 .await
321 }
322
323 async fn coin_balance(&self) -> Option<BigInt> {
325 Some(BigInt::from(self.native.balance.value()))
326 }
327}
328
329impl Coin {
330 pub(crate) async fn paginate(
333 db: &Db,
334 page: Page<object::Cursor>,
335 coin_type: TypeTag,
336 owner: Option<IotaAddress>,
337 checkpoint_viewed_at: u64,
338 ) -> Result<Connection<String, Coin>, Error> {
339 let cursor_viewed_at = page.validate_cursor_consistency()?;
344 let checkpoint_viewed_at = cursor_viewed_at.unwrap_or(checkpoint_viewed_at);
345
346 let Some((prev, next, results)) = db
347 .execute_repeatable(move |conn| {
348 let Some(range) = AvailableRange::result(conn, checkpoint_viewed_at)? else {
349 return Ok::<_, diesel::result::Error>(None);
350 };
351
352 Ok(Some(page.paginate_raw_query::<StoredHistoryObject>(
353 conn,
354 checkpoint_viewed_at,
355 coins_query(coin_type, owner, range, &page),
356 )?))
357 })
358 .await?
359 else {
360 return Err(Error::Client(
361 "Requested data is outside the available range".to_string(),
362 ));
363 };
364
365 let mut conn: Connection<String, Coin> = Connection::new(prev, next);
366
367 for stored in results {
368 let cursor = stored.cursor(checkpoint_viewed_at).encode_cursor();
371 let object =
372 Object::try_from_stored_history_object(stored, checkpoint_viewed_at, None)?;
373
374 let move_ = MoveObject::try_from(&object).map_err(|_| {
375 Error::Internal(format!(
376 "Failed to deserialize as Move object: {}",
377 object.address
378 ))
379 })?;
380
381 let coin = Coin::try_from(&move_).map_err(|_| {
382 Error::Internal(format!("Failed to deserialize as Coin: {}", object.address))
383 })?;
384
385 conn.edges.push(Edge::new(cursor, coin));
386 }
387
388 Ok(conn)
389 }
390}
391
392impl TryFrom<&MoveObject> for Coin {
393 type Error = CoinDowncastError;
394
395 fn try_from(move_object: &MoveObject) -> Result<Self, Self::Error> {
396 if !move_object.native.is_coin() {
397 return Err(CoinDowncastError::NotACoin);
398 }
399
400 Ok(Self {
401 super_: move_object.clone(),
402 native: bcs::from_bytes(move_object.native.contents())
403 .map_err(CoinDowncastError::Bcs)?,
404 })
405 }
406}
407
408fn coins_query(
412 coin_type: TypeTag,
413 owner: Option<IotaAddress>,
414 range: AvailableRange,
415 page: &Page<object::Cursor>,
416) -> RawQuery {
417 build_objects_query(
418 View::Consistent,
419 range,
420 page,
421 move |query| apply_filter(query, &coin_type, owner),
422 move |newer| newer,
423 )
424}
425
426fn apply_filter(mut query: RawQuery, coin_type: &TypeTag, owner: Option<IotaAddress>) -> RawQuery {
427 if let Some(owner) = owner {
428 query = filter!(
429 query,
430 format!(
431 "owner_id = '\\x{}'::bytea AND owner_type = {}",
432 hex::encode(owner.into_vec()),
433 OwnerType::Address as i16
434 )
435 );
436 }
437
438 query = filter!(
439 query,
440 "coin_type IS NOT NULL AND coin_type = {}",
441 coin_type.to_canonical_display(true)
442 );
443
444 query
445}