Skip to main content

iota_sdk_graphql_client/api/
iota_names.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2026 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5//! IOTA Names API implementation.
6
7use std::str::FromStr;
8
9use cynic::QueryBuilder;
10use futures::Stream;
11use iota_types::{
12    Address,
13    iota_names::{NameFormat, NameRegistration, name::Name},
14};
15
16use crate::{
17    GraphQLClient,
18    api::define_query,
19    error::{GraphQLError, GraphQLResult},
20    pagination::{Page, PaginationFilter, PaginationFilterResponse},
21    query_types::{
22        IotaNamesAddressDefaultNameQueryFragment, IotaNamesAddressRegistrationsQueryFragment,
23        IotaNamesDefaultNameArgs, IotaNamesDefaultNameQueryFragment, IotaNamesRegistrationsArgs,
24        IotaNamesRegistrationsQueryFragment, ResolveIotaNamesAddressArgs,
25        ResolveIotaNamesAddressQueryFragment,
26    },
27    streams::stream_paginated_query,
28};
29
30define_query! {
31    /// Query for [`GraphQLClient::iota_names_registrations`]. Await it to send
32    /// the request.
33    #[derive(Clone)]
34    pub struct ListIotaNamesRegistrationsQuery {
35        client: GraphQLClient,
36        address: Address,
37        pagination: PaginationFilter,
38    }
39    output: GraphQLResult<Page<NameRegistration>>;
40}
41
42impl ListIotaNamesRegistrationsQuery {
43    /// Set the page to fetch.
44    pub fn pagination(mut self, pagination: PaginationFilter) -> Self {
45        self.pagination = pagination;
46        self
47    }
48
49    /// Stream every item, page by page, starting at the pagination's cursor
50    /// and in its direction, with its limit as the page size.
51    pub fn stream(self) -> impl Stream<Item = GraphQLResult<NameRegistration>> + Unpin {
52        let pagination = self.pagination.clone();
53        stream_paginated_query(move |page| self.clone().pagination(page).send(), pagination)
54    }
55
56    fn operation(
57        address: Address,
58        pagination: PaginationFilterResponse,
59    ) -> cynic::Operation<IotaNamesAddressRegistrationsQueryFragment, IotaNamesRegistrationsArgs>
60    {
61        IotaNamesAddressRegistrationsQueryFragment::build(IotaNamesRegistrationsArgs {
62            address,
63            after: pagination.after,
64            before: pagination.before,
65            first: pagination.first,
66            last: pagination.last,
67        })
68    }
69
70    async fn send(self) -> GraphQLResult<Page<NameRegistration>> {
71        let Self {
72            client,
73            pagination,
74            address,
75        } = self;
76        let pagination = client.pagination_filter(pagination).await;
77        let response = client
78            .run_query(&Self::operation(address, pagination))
79            .await?;
80
81        let IotaNamesAddressRegistrationsQueryFragment {
82            address:
83                Some(IotaNamesRegistrationsQueryFragment {
84                    iota_names_registrations,
85                }),
86        } = response
87        else {
88            return Ok(Page::new_empty());
89        };
90
91        Ok(Page::new(
92            iota_names_registrations.page_info,
93            iota_names_registrations
94                .nodes
95                .into_iter()
96                .map(TryInto::try_into)
97                .collect::<GraphQLResult<Vec<_>>>()?,
98        ))
99    }
100}
101
102define_query! {
103    /// Query for [`GraphQLClient::iota_names_default_name`]. Await it to send
104    /// the request.
105    pub struct GetIotaNamesDefaultNameQuery {
106        client: GraphQLClient,
107        address: Address,
108        format: Option<NameFormat>,
109    }
110    output: GraphQLResult<Option<Name>>;
111}
112
113impl GetIotaNamesDefaultNameQuery {
114    /// Set the format of the returned name.
115    pub fn format(mut self, format: NameFormat) -> Self {
116        self.format = Some(format);
117        self
118    }
119
120    async fn send(self) -> GraphQLResult<Option<Name>> {
121        let operation = IotaNamesAddressDefaultNameQueryFragment::build(IotaNamesDefaultNameArgs {
122            address: self.address,
123            format: self.format.map(Into::into),
124        });
125        let response = self.client.run_query(&operation).await?;
126
127        let IotaNamesAddressDefaultNameQueryFragment {
128            address:
129                Some(IotaNamesDefaultNameQueryFragment {
130                    iota_names_default_name: Some(name),
131                }),
132        } = response
133        else {
134            return Ok(None);
135        };
136
137        Ok(Some(Name::from_str(&name).map_err(|source| {
138            GraphQLError::InvalidName { name, source }
139        })?))
140    }
141}
142
143define_query! {
144    /// Query for [`GraphQLClient::iota_names_lookup`]. Await it to send the
145    /// request.
146    pub struct GetIotaNamesLookupQuery {
147        client: GraphQLClient,
148        name: Name,
149    }
150    output: GraphQLResult<Option<Address>>;
151}
152
153impl GetIotaNamesLookupQuery {
154    async fn send(self) -> GraphQLResult<Option<Address>> {
155        let operation = ResolveIotaNamesAddressQueryFragment::build(ResolveIotaNamesAddressArgs {
156            name: self.name.to_string(),
157        });
158        let response = self.client.run_query(&operation).await?;
159
160        let ResolveIotaNamesAddressQueryFragment {
161            resolve_iota_names_address: Some(address),
162        } = response
163        else {
164            return Ok(None);
165        };
166
167        Ok(Some(address.address))
168    }
169}
170
171impl GraphQLClient {
172    /// Return the resolved address for the given name.
173    pub fn iota_names_lookup(&self, name: Name) -> GetIotaNamesLookupQuery {
174        GetIotaNamesLookupQuery {
175            client: self.clone(),
176            name,
177        }
178    }
179
180    /// Find all registration NFTs for the given address.
181    pub fn iota_names_registrations(&self, address: Address) -> ListIotaNamesRegistrationsQuery {
182        ListIotaNamesRegistrationsQuery {
183            client: self.clone(),
184            address,
185            pagination: PaginationFilter::default(),
186        }
187    }
188
189    /// Get the default name pointing to this address, if one exists.
190    pub fn iota_names_default_name(&self, address: Address) -> GetIotaNamesDefaultNameQuery {
191        GetIotaNamesDefaultNameQuery {
192            client: self.clone(),
193            address,
194            format: None,
195        }
196    }
197}
198
199#[cfg(all(test, not(target_arch = "wasm32")))]
200mod tests {
201    use iota_types::{Address, iota_names::NameFormat};
202
203    use crate::test_utils::{
204        assert_backward_page, assert_forward_page, backward_page, forward_page, sent_variables,
205    };
206
207    #[tokio::test]
208    async fn iota_names_lookup_sends_the_name() {
209        let vars = sent_variables(
210            "ResolveIotaNamesAddressQueryFragment",
211            |client| async move {
212                let _ = client
213                    .iota_names_lookup("example.iota".parse().unwrap())
214                    .await;
215            },
216        )
217        .await;
218        assert_eq!(vars["name"], "example.iota");
219    }
220
221    #[tokio::test]
222    async fn iota_names_default_name_sends_the_address_and_format() {
223        let vars = sent_variables(
224            "IotaNamesAddressDefaultNameQueryFragment",
225            |client| async move {
226                let _ = client
227                    .iota_names_default_name(Address::FRAMEWORK)
228                    .format(NameFormat::Dot)
229                    .await;
230            },
231        )
232        .await;
233        assert_eq!(vars["address"], Address::FRAMEWORK.to_string());
234        assert_eq!(vars["format"], "DOT");
235
236        let vars = sent_variables(
237            "IotaNamesAddressDefaultNameQueryFragment",
238            |client| async move {
239                let _ = client.iota_names_default_name(Address::FRAMEWORK).await;
240            },
241        )
242        .await;
243        assert!(vars["format"].is_null());
244    }
245
246    #[tokio::test]
247    async fn iota_names_registrations_sends_the_address_and_pagination() {
248        let vars = sent_variables(
249            "IotaNamesAddressRegistrationsQueryFragment",
250            |client| async move {
251                let _ = client
252                    .iota_names_registrations(Address::FRAMEWORK)
253                    .pagination(backward_page())
254                    .await;
255            },
256        )
257        .await;
258        assert_eq!(vars["address"], Address::FRAMEWORK.to_string());
259        assert_backward_page(&vars);
260
261        let vars = sent_variables(
262            "IotaNamesAddressRegistrationsQueryFragment",
263            |client| async move {
264                let _ = client
265                    .iota_names_registrations(Address::FRAMEWORK)
266                    .pagination(forward_page())
267                    .await;
268            },
269        )
270        .await;
271        assert_forward_page(&vars);
272    }
273}