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