Skip to main content

iota_graphql_rpc/types/
owner.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use async_graphql::{connection::Connection, *};
6use iota_names::config::IotaNamesConfig;
7use iota_sdk_types::{StructTag, TypeTag};
8use iota_types::dynamic_field::DynamicFieldType;
9
10use crate::{
11    data::Db,
12    types::{
13        address::Address,
14        balance::{self, Balance},
15        coin::Coin,
16        coin_metadata::CoinMetadata,
17        cursor::Page,
18        dynamic_field::{DynamicField, DynamicFieldName},
19        iota_address::IotaAddress,
20        iota_names_registration::{IotaNames, NameFormat, NameRegistration},
21        move_object::MoveObject,
22        move_package::MovePackage,
23        object::{self, Object, ObjectFilter},
24        stake::StakedIota,
25        type_filter::ExactTypeFilter,
26    },
27};
28
29#[derive(Clone, Debug)]
30pub(crate) struct Owner {
31    pub address: IotaAddress,
32    /// The checkpoint sequence number at which this was viewed at.
33    pub checkpoint_viewed_at: u64,
34    /// Root parent object version for dynamic fields.
35    ///
36    /// This enables consistent dynamic field reads in the case of chained
37    /// dynamic object fields, e.g., `Parent -> DOF1 -> DOF2`. In such
38    /// cases, the object versions may end up like `Parent >= DOF1, DOF2`
39    /// but `DOF1 < DOF2`. Thus, database queries for dynamic fields must
40    /// bound the object versions by the version of the root object of the tree.
41    ///
42    /// Also, if this Owner is an object itself, `root_version` will be used to
43    /// bound its version from above in [`Owner::as_object`].
44    ///
45    /// Essentially, lamport timestamps of objects are updated for all top-level
46    /// mutable objects provided as inputs to a transaction as well as any
47    /// mutated dynamic child objects. However, any dynamic child objects
48    /// that were loaded but not actually mutated don't end up having
49    /// their versions updated.
50    pub root_version: Option<u64>,
51}
52
53/// Type to implement GraphQL fields that are shared by all Owners.
54pub(crate) struct OwnerImpl {
55    pub address: IotaAddress,
56    /// The checkpoint sequence number at which this was viewed at.
57    pub checkpoint_viewed_at: u64,
58}
59
60/// Interface implemented by GraphQL types representing entities that can own
61/// objects. Object owners are identified by an address which can represent
62/// either the public key of an account or another object. The same address can
63/// only refer to an account or an object, never both, but it is not possible to
64/// know which up-front.
65#[expect(clippy::duplicated_attributes)]
66#[derive(Interface)]
67#[graphql(
68    name = "IOwner",
69    field(name = "address", ty = "IotaAddress"),
70    field(
71        name = "objects",
72        arg(name = "first", ty = "Option<u64>"),
73        arg(name = "after", ty = "Option<object::Cursor>"),
74        arg(name = "last", ty = "Option<u64>"),
75        arg(name = "before", ty = "Option<object::Cursor>"),
76        arg(name = "filter", ty = "Option<ObjectFilter>"),
77        ty = "Connection<String, MoveObject>",
78        desc = "Objects owned by this object or address, optionally `filter`-ed."
79    ),
80    field(
81        name = "balance",
82        arg(name = "type", ty = "Option<ExactTypeFilter>"),
83        ty = "Option<Balance>",
84        desc = "Total balance of all coins with marker type owned by this object or address. If \
85                type is not supplied, it defaults to `0x2::iota::IOTA`."
86    ),
87    field(
88        name = "balances",
89        arg(name = "first", ty = "Option<u64>"),
90        arg(name = "after", ty = "Option<balance::Cursor>"),
91        arg(name = "last", ty = "Option<u64>"),
92        arg(name = "before", ty = "Option<balance::Cursor>"),
93        ty = "Connection<String, Balance>",
94        desc = "The balances of all coin types owned by this object or address."
95    ),
96    field(
97        name = "coins",
98        arg(name = "first", ty = "Option<u64>"),
99        arg(name = "after", ty = "Option<object::Cursor>"),
100        arg(name = "last", ty = "Option<u64>"),
101        arg(name = "before", ty = "Option<object::Cursor>"),
102        arg(name = "type", ty = "Option<ExactTypeFilter>"),
103        ty = "Connection<String, Coin>",
104        desc = "The coin objects for this object or address.\n\n\
105                `type` is a filter on the coin's type parameter, defaulting to `0x2::iota::IOTA`."
106    ),
107    field(
108        name = "staked_iotas",
109        arg(name = "first", ty = "Option<u64>"),
110        arg(name = "after", ty = "Option<object::Cursor>"),
111        arg(name = "last", ty = "Option<u64>"),
112        arg(name = "before", ty = "Option<object::Cursor>"),
113        ty = "Connection<String, StakedIota>",
114        desc = "The `0x3::staking_pool::StakedIota` objects owned by this object or address."
115    ),
116    field(
117        name = "iota_names_default_name",
118        arg(name = "format", ty = "Option<NameFormat>"),
119        ty = "Option<String>",
120        desc = "The name explicitly configured as the default name pointing to this object or \
121                    address."
122    ),
123    field(
124        name = "iota_names_registrations",
125        arg(name = "first", ty = "Option<u64>"),
126        arg(name = "after", ty = "Option<object::Cursor>"),
127        arg(name = "last", ty = "Option<u64>"),
128        arg(name = "before", ty = "Option<object::Cursor>"),
129        ty = "Connection<String, NameRegistration>",
130        desc = "The NameRegistration NFTs owned by this object or address. These grant the owner \
131                    the capability to manage the associated name."
132    )
133)]
134pub(crate) enum IOwner {
135    Owner(Owner),
136    Address(Address),
137    Object(Object),
138    MovePackage(MovePackage),
139    MoveObject(MoveObject),
140    Coin(Coin),
141    CoinMetadata(CoinMetadata),
142    StakedIota(StakedIota),
143    NameRegistration(NameRegistration),
144}
145
146/// An Owner is an entity that can own an object. Each Owner is identified by a
147/// IotaAddress which represents either an Address (corresponding to a public
148/// key of an account) or an Object, but never both (it is not known up-front
149/// whether a given Owner is an Address or an Object).
150#[Object]
151impl Owner {
152    pub(crate) async fn address(&self) -> IotaAddress {
153        OwnerImpl::from(self).address().await
154    }
155
156    /// Objects owned by this object or address, optionally `filter`-ed.
157    pub(crate) async fn objects(
158        &self,
159        ctx: &Context<'_>,
160        first: Option<u64>,
161        after: Option<object::Cursor>,
162        last: Option<u64>,
163        before: Option<object::Cursor>,
164        filter: Option<ObjectFilter>,
165    ) -> Result<Connection<String, MoveObject>> {
166        OwnerImpl::from(self)
167            .objects(ctx, first, after, last, before, filter)
168            .await
169    }
170
171    /// Total balance of all coins with marker type owned by this object or
172    /// address. If type is not supplied, it defaults to `0x2::iota::IOTA`.
173    pub(crate) async fn balance(
174        &self,
175        ctx: &Context<'_>,
176        type_: Option<ExactTypeFilter>,
177    ) -> Result<Option<Balance>> {
178        OwnerImpl::from(self).balance(ctx, type_).await
179    }
180
181    /// The balances of all coin types owned by this object or address.
182    pub(crate) async fn balances(
183        &self,
184        ctx: &Context<'_>,
185        first: Option<u64>,
186        after: Option<balance::Cursor>,
187        last: Option<u64>,
188        before: Option<balance::Cursor>,
189    ) -> Result<Connection<String, Balance>> {
190        OwnerImpl::from(self)
191            .balances(ctx, first, after, last, before)
192            .await
193    }
194
195    /// The coin objects for this object or address.
196    ///
197    /// `type` is a filter on the coin's type parameter, defaulting to
198    /// `0x2::iota::IOTA`.
199    pub(crate) async fn coins(
200        &self,
201        ctx: &Context<'_>,
202        first: Option<u64>,
203        after: Option<object::Cursor>,
204        last: Option<u64>,
205        before: Option<object::Cursor>,
206        type_: Option<ExactTypeFilter>,
207    ) -> Result<Connection<String, Coin>> {
208        OwnerImpl::from(self)
209            .coins(ctx, first, after, last, before, type_)
210            .await
211    }
212
213    /// The `0x3::staking_pool::StakedIota` objects owned by this object or
214    /// address.
215    pub(crate) async fn staked_iotas(
216        &self,
217        ctx: &Context<'_>,
218        first: Option<u64>,
219        after: Option<object::Cursor>,
220        last: Option<u64>,
221        before: Option<object::Cursor>,
222    ) -> Result<Connection<String, StakedIota>> {
223        OwnerImpl::from(self)
224            .staked_iotas(ctx, first, after, last, before)
225            .await
226    }
227
228    /// The name explicitly configured as the default name pointing to this
229    /// object or address.
230    pub(crate) async fn iota_names_default_name(
231        &self,
232        ctx: &Context<'_>,
233        format: Option<NameFormat>,
234    ) -> Result<Option<String>> {
235        OwnerImpl::from(self)
236            .iota_names_default_name(ctx, format)
237            .await
238    }
239
240    /// The NameRegistration NFTs owned by this object or address. These
241    /// grant the owner the capability to manage the associated name.
242    pub(crate) async fn iota_names_registrations(
243        &self,
244        ctx: &Context<'_>,
245        first: Option<u64>,
246        after: Option<object::Cursor>,
247        last: Option<u64>,
248        before: Option<object::Cursor>,
249    ) -> Result<Connection<String, NameRegistration>> {
250        OwnerImpl::from(self)
251            .iota_names_registrations(ctx, first, after, last, before)
252            .await
253    }
254
255    async fn as_address(&self) -> Option<Address> {
256        // For now only addresses can be owners
257        Some(Address {
258            address: self.address,
259            checkpoint_viewed_at: self.checkpoint_viewed_at,
260        })
261    }
262
263    async fn as_object(&self, ctx: &Context<'_>) -> Result<Option<Object>> {
264        Object::query(
265            ctx,
266            self.address,
267            if let Some(parent_version) = self.root_version {
268                Object::under_parent(parent_version, self.checkpoint_viewed_at)
269            } else {
270                Object::latest_at(self.checkpoint_viewed_at)
271            },
272        )
273        .await
274        .extend()
275    }
276
277    /// Access a dynamic field on an object using its name. Names are arbitrary
278    /// Move values whose type have `copy`, `drop`, and `store`, and are
279    /// specified using their type, and their BCS contents, Base64 encoded.
280    ///
281    /// This field exists as a convenience when accessing a dynamic field on a
282    /// wrapped object.
283    async fn dynamic_field(
284        &self,
285        ctx: &Context<'_>,
286        name: DynamicFieldName,
287    ) -> Result<Option<DynamicField>> {
288        OwnerImpl::from(self)
289            .dynamic_field(ctx, name, self.root_version)
290            .await
291    }
292
293    /// Access a dynamic object field on an object using its name. Names are
294    /// arbitrary Move values whose type have `copy`, `drop`, and `store`,
295    /// and are specified using their type, and their BCS contents, Base64
296    /// encoded. The value of a dynamic object field can also be accessed
297    /// off-chain directly via its address (e.g. using `Query.object`).
298    ///
299    /// This field exists as a convenience when accessing a dynamic field on a
300    /// wrapped object.
301    async fn dynamic_object_field(
302        &self,
303        ctx: &Context<'_>,
304        name: DynamicFieldName,
305    ) -> Result<Option<DynamicField>> {
306        OwnerImpl::from(self)
307            .dynamic_object_field(ctx, name, self.root_version)
308            .await
309    }
310
311    /// The dynamic fields and dynamic object fields on an object.
312    ///
313    /// This field exists as a convenience when accessing a dynamic field on a
314    /// wrapped object.
315    async fn dynamic_fields(
316        &self,
317        ctx: &Context<'_>,
318        first: Option<u64>,
319        after: Option<object::Cursor>,
320        last: Option<u64>,
321        before: Option<object::Cursor>,
322    ) -> Result<Connection<String, DynamicField>> {
323        OwnerImpl::from(self)
324            .dynamic_fields(ctx, first, after, last, before, self.root_version)
325            .await
326    }
327}
328
329impl OwnerImpl {
330    pub(crate) async fn address(&self) -> IotaAddress {
331        self.address
332    }
333
334    pub(crate) async fn objects(
335        &self,
336        ctx: &Context<'_>,
337        first: Option<u64>,
338        after: Option<object::Cursor>,
339        last: Option<u64>,
340        before: Option<object::Cursor>,
341        filter: Option<ObjectFilter>,
342    ) -> Result<Connection<String, MoveObject>> {
343        let page = Page::from_params(ctx.data_unchecked(), first, after, last, before)?;
344
345        let Some(filter) = filter.unwrap_or_default().intersect(ObjectFilter {
346            owner: Some(self.address),
347            ..Default::default()
348        }) else {
349            return Ok(Connection::new(false, false));
350        };
351
352        MoveObject::paginate(
353            ctx.data_unchecked(),
354            page,
355            filter,
356            self.checkpoint_viewed_at,
357        )
358        .await
359        .extend()
360    }
361
362    pub(crate) async fn balance(
363        &self,
364        ctx: &Context<'_>,
365        type_: Option<ExactTypeFilter>,
366    ) -> Result<Option<Balance>> {
367        let coin = type_.map_or_else(|| TypeTag::from(StructTag::new_gas()), |t| t.0);
368        Balance::query(
369            ctx.data_unchecked(),
370            self.address,
371            coin,
372            self.checkpoint_viewed_at,
373        )
374        .await
375        .extend()
376    }
377
378    pub(crate) async fn balances(
379        &self,
380        ctx: &Context<'_>,
381        first: Option<u64>,
382        after: Option<balance::Cursor>,
383        last: Option<u64>,
384        before: Option<balance::Cursor>,
385    ) -> Result<Connection<String, Balance>> {
386        let page = Page::from_params(ctx.data_unchecked(), first, after, last, before)?;
387        Balance::paginate(
388            ctx.data_unchecked(),
389            page,
390            self.address,
391            self.checkpoint_viewed_at,
392        )
393        .await
394        .extend()
395    }
396
397    pub(crate) async fn coins(
398        &self,
399        ctx: &Context<'_>,
400        first: Option<u64>,
401        after: Option<object::Cursor>,
402        last: Option<u64>,
403        before: Option<object::Cursor>,
404        type_: Option<ExactTypeFilter>,
405    ) -> Result<Connection<String, Coin>> {
406        let page = Page::from_params(ctx.data_unchecked(), first, after, last, before)?;
407        let coin = type_.map_or_else(|| TypeTag::from(StructTag::new_gas()), |t| t.0);
408        Coin::paginate(
409            ctx.data_unchecked(),
410            page,
411            coin,
412            Some(self.address),
413            self.checkpoint_viewed_at,
414        )
415        .await
416        .extend()
417    }
418
419    pub(crate) async fn staked_iotas(
420        &self,
421        ctx: &Context<'_>,
422        first: Option<u64>,
423        after: Option<object::Cursor>,
424        last: Option<u64>,
425        before: Option<object::Cursor>,
426    ) -> Result<Connection<String, StakedIota>> {
427        let page = Page::from_params(ctx.data_unchecked(), first, after, last, before)?;
428        StakedIota::paginate(
429            ctx.data_unchecked(),
430            page,
431            self.address,
432            self.checkpoint_viewed_at,
433        )
434        .await
435        .extend()
436    }
437
438    pub(crate) async fn iota_names_default_name(
439        &self,
440        ctx: &Context<'_>,
441        format: Option<NameFormat>,
442    ) -> Result<Option<String>> {
443        Ok(
444            IotaNames::reverse_resolve_to_name(ctx, self.address, self.checkpoint_viewed_at)
445                .await
446                .extend()?
447                .map(|d| d.format(format.unwrap_or(NameFormat::Dot).into())),
448        )
449    }
450
451    pub(crate) async fn iota_names_registrations(
452        &self,
453        ctx: &Context<'_>,
454        first: Option<u64>,
455        after: Option<object::Cursor>,
456        last: Option<u64>,
457        before: Option<object::Cursor>,
458    ) -> Result<Connection<String, NameRegistration>> {
459        let page = Page::from_params(ctx.data_unchecked(), first, after, last, before)?;
460        NameRegistration::paginate(
461            ctx.data_unchecked::<Db>(),
462            ctx.data_unchecked::<IotaNamesConfig>(),
463            page,
464            self.address,
465            self.checkpoint_viewed_at,
466        )
467        .await
468        .extend()
469    }
470
471    // Dynamic field related functions are part of the `IMoveObject` interface, but
472    // are provided here to implement convenience functions on `Owner` and
473    // `Object` to access dynamic fields.
474
475    pub(crate) async fn dynamic_field(
476        &self,
477        ctx: &Context<'_>,
478        name: DynamicFieldName,
479        parent_version: Option<u64>,
480    ) -> Result<Option<DynamicField>> {
481        use DynamicFieldType as T;
482        DynamicField::query(
483            ctx,
484            self.address,
485            parent_version,
486            name,
487            T::DynamicField,
488            self.checkpoint_viewed_at,
489        )
490        .await
491        .extend()
492    }
493
494    pub(crate) async fn dynamic_object_field(
495        &self,
496        ctx: &Context<'_>,
497        name: DynamicFieldName,
498        parent_version: Option<u64>,
499    ) -> Result<Option<DynamicField>> {
500        use DynamicFieldType as T;
501        DynamicField::query(
502            ctx,
503            self.address,
504            parent_version,
505            name,
506            T::DynamicObject,
507            self.checkpoint_viewed_at,
508        )
509        .await
510        .extend()
511    }
512
513    pub(crate) async fn dynamic_fields(
514        &self,
515        ctx: &Context<'_>,
516        first: Option<u64>,
517        after: Option<object::Cursor>,
518        last: Option<u64>,
519        before: Option<object::Cursor>,
520        parent_version: Option<u64>,
521    ) -> Result<Connection<String, DynamicField>> {
522        let page = Page::from_params(ctx.data_unchecked(), first, after, last, before)?;
523        DynamicField::paginate(
524            ctx.data_unchecked(),
525            page,
526            self.address,
527            parent_version,
528            self.checkpoint_viewed_at,
529        )
530        .await
531        .extend()
532    }
533}
534
535impl From<&Owner> for OwnerImpl {
536    fn from(owner: &Owner) -> Self {
537        OwnerImpl {
538            address: owner.address,
539            checkpoint_viewed_at: owner.checkpoint_viewed_at,
540        }
541    }
542}