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::{DomainFormat, IotaNamesRegistration},
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<DomainFormat>,
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, IotaNamesRegistration>> {
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 {
176 ObjectImpl(&self.super_.super_).status().await
177 }
178
179 pub(crate) async fn digest(&self) -> Option<String> {
182 ObjectImpl(&self.super_.super_).digest().await
183 }
184
185 pub(crate) async fn owner(&self, ctx: &Context<'_>) -> Option<ObjectOwner> {
187 ObjectImpl(&self.super_.super_).owner(ctx).await
188 }
189
190 pub(crate) async fn previous_transaction_block(
192 &self,
193 ctx: &Context<'_>,
194 ) -> Result<Option<TransactionBlock>> {
195 ObjectImpl(&self.super_.super_)
196 .previous_transaction_block(ctx)
197 .await
198 }
199
200 pub(crate) async fn storage_rebate(&self) -> Option<BigInt> {
204 ObjectImpl(&self.super_.super_).storage_rebate().await
205 }
206
207 pub(crate) async fn received_transaction_blocks(
233 &self,
234 ctx: &Context<'_>,
235 first: Option<u64>,
236 after: Option<transaction_block::Cursor>,
237 last: Option<u64>,
238 before: Option<transaction_block::Cursor>,
239 filter: Option<TransactionBlockFilter>,
240 scan_limit: Option<u64>,
241 ) -> Result<ScanConnection<String, TransactionBlock>> {
242 ObjectImpl(&self.super_.super_)
243 .received_transaction_blocks(ctx, first, after, last, before, filter, scan_limit)
244 .await
245 }
246
247 pub(crate) async fn bcs(&self) -> Result<Option<Base64>> {
249 ObjectImpl(&self.super_.super_).bcs().await
250 }
251
252 pub(crate) async fn contents(&self) -> Option<MoveValue> {
256 MoveObjectImpl(&self.super_).contents().await
257 }
258
259 pub(crate) async fn display(&self, ctx: &Context<'_>) -> Result<Option<Vec<DisplayEntry>>> {
263 ObjectImpl(&self.super_.super_).display(ctx).await
264 }
265
266 pub(crate) async fn dynamic_field(
273 &self,
274 ctx: &Context<'_>,
275 name: DynamicFieldName,
276 ) -> Result<Option<DynamicField>> {
277 OwnerImpl::from(&self.super_.super_)
278 .dynamic_field(ctx, name, Some(self.super_.root_version()))
279 .await
280 }
281
282 pub(crate) async fn dynamic_object_field(
291 &self,
292 ctx: &Context<'_>,
293 name: DynamicFieldName,
294 ) -> Result<Option<DynamicField>> {
295 OwnerImpl::from(&self.super_.super_)
296 .dynamic_object_field(ctx, name, Some(self.super_.root_version()))
297 .await
298 }
299
300 pub(crate) async fn dynamic_fields(
305 &self,
306 ctx: &Context<'_>,
307 first: Option<u64>,
308 after: Option<object::Cursor>,
309 last: Option<u64>,
310 before: Option<object::Cursor>,
311 ) -> Result<Connection<String, DynamicField>> {
312 OwnerImpl::from(&self.super_.super_)
313 .dynamic_fields(
314 ctx,
315 first,
316 after,
317 last,
318 before,
319 Some(self.super_.root_version()),
320 )
321 .await
322 }
323
324 async fn coin_balance(&self) -> Option<BigInt> {
326 Some(BigInt::from(self.native.balance.value()))
327 }
328}
329
330impl Coin {
331 pub(crate) async fn paginate(
334 db: &Db,
335 page: Page<object::Cursor>,
336 coin_type: TypeTag,
337 owner: Option<IotaAddress>,
338 checkpoint_viewed_at: u64,
339 ) -> Result<Connection<String, Coin>, Error> {
340 let cursor_viewed_at = page.validate_cursor_consistency()?;
345 let checkpoint_viewed_at = cursor_viewed_at.unwrap_or(checkpoint_viewed_at);
346
347 let Some((prev, next, results)) = db
348 .execute_repeatable(move |conn| {
349 let Some(range) = AvailableRange::result(conn, checkpoint_viewed_at)? else {
350 return Ok::<_, diesel::result::Error>(None);
351 };
352
353 Ok(Some(page.paginate_raw_query::<StoredHistoryObject>(
354 conn,
355 checkpoint_viewed_at,
356 coins_query(coin_type, owner, range, &page),
357 )?))
358 })
359 .await?
360 else {
361 return Err(Error::Client(
362 "Requested data is outside the available range".to_string(),
363 ));
364 };
365
366 let mut conn: Connection<String, Coin> = Connection::new(prev, next);
367
368 for stored in results {
369 let cursor = stored.cursor(checkpoint_viewed_at).encode_cursor();
372 let object =
373 Object::try_from_stored_history_object(stored, checkpoint_viewed_at, None)?;
374
375 let move_ = MoveObject::try_from(&object).map_err(|_| {
376 Error::Internal(format!(
377 "Failed to deserialize as Move object: {}",
378 object.address
379 ))
380 })?;
381
382 let coin = Coin::try_from(&move_).map_err(|_| {
383 Error::Internal(format!("Failed to deserialize as Coin: {}", object.address))
384 })?;
385
386 conn.edges.push(Edge::new(cursor, coin));
387 }
388
389 Ok(conn)
390 }
391}
392
393impl TryFrom<&MoveObject> for Coin {
394 type Error = CoinDowncastError;
395
396 fn try_from(move_object: &MoveObject) -> Result<Self, Self::Error> {
397 if !move_object.native.is_coin() {
398 return Err(CoinDowncastError::NotACoin);
399 }
400
401 Ok(Self {
402 super_: move_object.clone(),
403 native: bcs::from_bytes(move_object.native.contents())
404 .map_err(CoinDowncastError::Bcs)?,
405 })
406 }
407}
408
409fn coins_query(
413 coin_type: TypeTag,
414 owner: Option<IotaAddress>,
415 range: AvailableRange,
416 page: &Page<object::Cursor>,
417) -> RawQuery {
418 build_objects_query(
419 View::Consistent,
420 range,
421 page,
422 move |query| apply_filter(query, &coin_type, owner),
423 move |newer| newer,
424 )
425}
426
427fn apply_filter(mut query: RawQuery, coin_type: &TypeTag, owner: Option<IotaAddress>) -> RawQuery {
428 if let Some(owner) = owner {
429 query = filter!(
430 query,
431 format!(
432 "owner_id = '\\x{}'::bytea AND owner_type = {}",
433 hex::encode(owner.into_vec()),
434 OwnerType::Address as i16
435 )
436 );
437 }
438
439 query = filter!(
440 query,
441 "coin_type IS NOT NULL AND coin_type = {}",
442 coin_type.to_canonical_display(true)
443 );
444
445 query
446}