iota_sdk/client/node_api/indexer/
query_parameters.rs

1// Copyright 2022 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4//! Query parameters for output_id requests
5
6use std::fmt;
7
8use serde::{Deserialize, Serialize};
9
10use crate::{
11    client::{Error, Result},
12    types::block::address::Bech32Address,
13};
14
15// https://github.com/iotaledger/inx-indexer/tree/develop/pkg/indexer
16
17/// Query parameters for output_id requests.
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct QueryParameters(Vec<QueryParameter>);
20
21impl QueryParameters {
22    /// Creates a hashset from a provided vec of query parameters.
23    #[must_use]
24    pub fn new(query_parameters: impl Into<Vec<QueryParameter>>) -> Self {
25        let mut query_parameters = query_parameters.into();
26        query_parameters.sort_unstable_by_key(QueryParameter::kind);
27        query_parameters.dedup_by_key(|qp| qp.kind());
28
29        Self(query_parameters)
30    }
31
32    /// Creates new empty QueryParameters.
33    pub fn empty() -> Self {
34        Self(Vec::new())
35    }
36
37    /// Replaces or inserts an enum variant in the QueryParameters.
38    pub fn replace(&mut self, query_parameter: QueryParameter) {
39        match self
40            .0
41            .binary_search_by_key(&query_parameter.kind(), QueryParameter::kind)
42        {
43            Ok(pos) => self.0[pos] = query_parameter,
44            Err(pos) => self.0.insert(pos, query_parameter),
45        }
46    }
47
48    /// Returns true if the slice contains an element with the given kind.
49    pub(crate) fn contains(&self, kind: u8) -> bool {
50        self.0.iter().any(|q| q.kind() == kind)
51    }
52
53    // Tests if any query parameter matches a predicate.
54    #[cfg(test)]
55    pub(crate) fn any<F: Fn(&QueryParameter) -> bool>(&self, f: F) -> bool {
56        self.0.iter().any(f)
57    }
58
59    /// Converts parameters to a single String.
60    pub fn to_query_string(&self) -> Option<String> {
61        if self.0.is_empty() {
62            None
63        } else {
64            Some(
65                self.0
66                    .iter()
67                    .map(QueryParameter::to_query_string)
68                    .collect::<Vec<String>>()
69                    .join("&"),
70            )
71        }
72    }
73}
74
75/// Query parameter for output requests.
76#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
77#[serde(rename_all = "camelCase")]
78#[non_exhaustive]
79pub enum QueryParameter {
80    /// Bech32-encoded address that should be searched for.
81    Address(Bech32Address),
82    /// Filter foundry outputs based on bech32-encoded address of the controlling alias.
83    AliasAddress(Bech32Address),
84    /// Returns outputs that were created after a certain Unix timestamp.
85    CreatedAfter(u32),
86    /// Returns outputs that were created before a certain Unix timestamp.
87    CreatedBefore(u32),
88    /// Starts the search from the cursor (confirmationMS+outputId.pageSize).
89    Cursor(String),
90    /// Filters outputs based on the presence of a specific Bech32-encoded return address in the expiration unlock
91    /// condition.
92    ExpirationReturnAddress(Bech32Address),
93    /// Returns outputs that expire after a certain Unix timestamp.
94    ExpiresAfter(u32),
95    /// Returns outputs that expire before a certain Unix timestamp.
96    ExpiresBefore(u32),
97    /// Filters outputs based on bech32-encoded governor (governance controller) address.
98    Governor(Bech32Address),
99    /// Filters outputs based on the presence of expiration unlock condition.
100    HasExpiration(bool),
101    /// Filters outputs based on the presence of native tokens.
102    HasNativeTokens(bool),
103    /// Filters outputs based on the presence of storage deposit return unlock condition.
104    HasStorageDepositReturn(bool),
105    /// Filters outputs based on the presence of timelock unlock condition.
106    HasTimelock(bool),
107    /// Filters outputs based on bech32-encoded issuer address.
108    Issuer(Bech32Address),
109    /// Filters outputs that have at most a certain number of distinct native tokens.
110    MaxNativeTokenCount(u32),
111    /// Filters outputs that have at least a certain number of distinct native tokens.
112    MinNativeTokenCount(u32),
113    /// The maximum amount of items returned in one call. If there are more items, a cursor to the next page is
114    /// returned too. The parameter is ignored when pageSize is defined via the cursor parameter.
115    PageSize(usize),
116    /// Filters outputs based on the presence of validated Sender (bech32 encoded).
117    Sender(Bech32Address),
118    /// Filters outputs based on bech32-encoded state controller address.
119    StateController(Bech32Address),
120    /// Filters outputs based on the presence of a specific return address in the storage deposit return unlock
121    /// condition.
122    StorageDepositReturnAddress(Bech32Address),
123    /// Filters outputs based on matching Tag Block.
124    Tag(String),
125    /// Returns outputs that are timelocked after a certain Unix timestamp.
126    TimelockedAfter(u32),
127    /// Returns outputs that are timelocked before a certain Unix timestamp.
128    TimelockedBefore(u32),
129    /// Returns outputs that are unlockable by the bech32 address.
130    UnlockableByAddress(Bech32Address),
131}
132
133impl QueryParameter {
134    fn to_query_string(&self) -> String {
135        match self {
136            Self::Address(v) => format!("address={v}"),
137            Self::AliasAddress(v) => format!("aliasAddress={v}"),
138            Self::CreatedAfter(v) => format!("createdAfter={v}"),
139            Self::CreatedBefore(v) => format!("createdBefore={v}"),
140            Self::Cursor(v) => format!("cursor={v}"),
141            Self::ExpirationReturnAddress(v) => format!("expirationReturnAddress={v}"),
142            Self::ExpiresAfter(v) => format!("expiresAfter={v}"),
143            Self::ExpiresBefore(v) => format!("expiresBefore={v}"),
144            Self::Governor(v) => format!("governor={v}"),
145            Self::HasExpiration(v) => format!("hasExpiration={v}"),
146            Self::HasNativeTokens(v) => format!("hasNativeTokens={v}"),
147            Self::HasStorageDepositReturn(v) => format!("hasStorageDepositReturn={v}"),
148            Self::HasTimelock(v) => format!("hasTimelock={v}"),
149            Self::Issuer(v) => format!("issuer={v}"),
150            Self::MaxNativeTokenCount(v) => format!("maxNativeTokenCount={v}"),
151            Self::MinNativeTokenCount(v) => format!("minNativeTokenCount={v}"),
152            Self::PageSize(v) => format!("pageSize={v}"),
153            Self::Sender(v) => format!("sender={v}"),
154            Self::StateController(v) => format!("stateController={v}"),
155            Self::StorageDepositReturnAddress(v) => format!("storageDepositReturnAddress={v}"),
156            Self::Tag(v) => format!("tag={v}"),
157            Self::TimelockedAfter(v) => format!("timelockedAfter={v}"),
158            Self::TimelockedBefore(v) => format!("timelockedBefore={v}"),
159            Self::UnlockableByAddress(v) => format!("unlockableByAddress={v}"),
160        }
161    }
162
163    pub(crate) fn kind(&self) -> u8 {
164        match self {
165            Self::Address(_) => 0,
166            Self::AliasAddress(_) => 1,
167            Self::CreatedAfter(_) => 2,
168            Self::CreatedBefore(_) => 3,
169            Self::Cursor(_) => 4,
170            Self::ExpirationReturnAddress(_) => 5,
171            Self::ExpiresAfter(_) => 6,
172            Self::ExpiresBefore(_) => 7,
173            Self::Governor(_) => 8,
174            Self::HasExpiration(_) => 9,
175            Self::HasNativeTokens(_) => 10,
176            Self::HasStorageDepositReturn(_) => 11,
177            Self::HasTimelock(_) => 12,
178            Self::Issuer(_) => 13,
179            Self::MaxNativeTokenCount(_) => 14,
180            Self::MinNativeTokenCount(_) => 15,
181            Self::PageSize(_) => 16,
182            Self::Sender(_) => 17,
183            Self::StateController(_) => 18,
184            Self::StorageDepositReturnAddress(_) => 19,
185            Self::Tag(_) => 20,
186            Self::TimelockedAfter(_) => 21,
187            Self::TimelockedBefore(_) => 22,
188            Self::UnlockableByAddress(_) => 23,
189        }
190    }
191}
192
193impl fmt::Display for QueryParameter {
194    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
195        write!(f, "{}", self.to_query_string())
196    }
197}
198
199macro_rules! verify_query_parameters {
200    ($query_parameters:ident, $first:path $(, $rest:path)*) => {
201        if let Some(qp) = $query_parameters.iter().find(|qp| {
202            !matches!(qp, $first(_) $(| $rest(_))*)
203        }) {
204            Err(Error::UnsupportedQueryParameter(qp.clone()))
205        } else {
206            Ok(())
207        }
208    };
209}
210
211pub(crate) fn verify_query_parameters_outputs(query_parameters: Vec<QueryParameter>) -> Result<QueryParameters> {
212    verify_query_parameters!(
213        query_parameters,
214        QueryParameter::HasNativeTokens,
215        QueryParameter::MinNativeTokenCount,
216        QueryParameter::MaxNativeTokenCount,
217        QueryParameter::CreatedBefore,
218        QueryParameter::CreatedAfter,
219        QueryParameter::PageSize,
220        QueryParameter::Cursor,
221        QueryParameter::UnlockableByAddress
222    )?;
223
224    Ok(QueryParameters::new(query_parameters))
225}
226
227pub(crate) fn verify_query_parameters_basic_outputs(query_parameters: Vec<QueryParameter>) -> Result<QueryParameters> {
228    verify_query_parameters!(
229        query_parameters,
230        QueryParameter::Address,
231        QueryParameter::HasNativeTokens,
232        QueryParameter::MinNativeTokenCount,
233        QueryParameter::MaxNativeTokenCount,
234        QueryParameter::HasStorageDepositReturn,
235        QueryParameter::StorageDepositReturnAddress,
236        QueryParameter::HasTimelock,
237        QueryParameter::TimelockedBefore,
238        QueryParameter::TimelockedAfter,
239        QueryParameter::HasExpiration,
240        QueryParameter::ExpiresBefore,
241        QueryParameter::ExpiresAfter,
242        QueryParameter::ExpirationReturnAddress,
243        QueryParameter::Sender,
244        QueryParameter::Tag,
245        QueryParameter::CreatedBefore,
246        QueryParameter::CreatedAfter,
247        QueryParameter::PageSize,
248        QueryParameter::Cursor,
249        QueryParameter::UnlockableByAddress
250    )?;
251
252    Ok(QueryParameters::new(query_parameters))
253}
254
255pub(crate) fn verify_query_parameters_alias_outputs(query_parameters: Vec<QueryParameter>) -> Result<QueryParameters> {
256    verify_query_parameters!(
257        query_parameters,
258        QueryParameter::StateController,
259        QueryParameter::Governor,
260        QueryParameter::Issuer,
261        QueryParameter::Sender,
262        QueryParameter::HasNativeTokens,
263        QueryParameter::MinNativeTokenCount,
264        QueryParameter::MaxNativeTokenCount,
265        QueryParameter::CreatedBefore,
266        QueryParameter::CreatedAfter,
267        QueryParameter::PageSize,
268        QueryParameter::Cursor,
269        QueryParameter::UnlockableByAddress
270    )?;
271
272    Ok(QueryParameters::new(query_parameters))
273}
274
275pub(crate) fn verify_query_parameters_foundry_outputs(
276    query_parameters: Vec<QueryParameter>,
277) -> Result<QueryParameters> {
278    verify_query_parameters!(
279        query_parameters,
280        QueryParameter::AliasAddress,
281        QueryParameter::HasNativeTokens,
282        QueryParameter::MinNativeTokenCount,
283        QueryParameter::MaxNativeTokenCount,
284        QueryParameter::CreatedBefore,
285        QueryParameter::CreatedAfter,
286        QueryParameter::PageSize,
287        QueryParameter::Cursor
288    )?;
289
290    Ok(QueryParameters::new(query_parameters))
291}
292
293pub(crate) fn verify_query_parameters_nft_outputs(query_parameters: Vec<QueryParameter>) -> Result<QueryParameters> {
294    verify_query_parameters!(
295        query_parameters,
296        QueryParameter::Address,
297        QueryParameter::HasNativeTokens,
298        QueryParameter::MinNativeTokenCount,
299        QueryParameter::MaxNativeTokenCount,
300        QueryParameter::HasStorageDepositReturn,
301        QueryParameter::StorageDepositReturnAddress,
302        QueryParameter::HasTimelock,
303        QueryParameter::TimelockedBefore,
304        QueryParameter::TimelockedAfter,
305        QueryParameter::HasExpiration,
306        QueryParameter::ExpiresBefore,
307        QueryParameter::ExpiresAfter,
308        QueryParameter::ExpirationReturnAddress,
309        QueryParameter::Issuer,
310        QueryParameter::Sender,
311        QueryParameter::Tag,
312        QueryParameter::CreatedBefore,
313        QueryParameter::CreatedAfter,
314        QueryParameter::PageSize,
315        QueryParameter::Cursor,
316        QueryParameter::UnlockableByAddress
317    )?;
318
319    Ok(QueryParameters::new(query_parameters))
320}
321
322#[cfg(test)]
323mod tests {
324    use super::*;
325
326    #[test]
327    fn query_parameter() {
328        let address1 = QueryParameter::Address(
329            Bech32Address::try_from_str("atoi1qzt0nhsf38nh6rs4p6zs5knqp6psgha9wsv74uajqgjmwc75ugupx3y7x0r").unwrap(),
330        );
331        let address2 = QueryParameter::Address(
332            Bech32Address::try_from_str("atoi1qzt0nhsf38nh6rs4p6zs5knqp6psgha9wsv74uajqgjmwc75ugupx3y7x0r").unwrap(),
333        );
334        let address3 = QueryParameter::Address(
335            Bech32Address::try_from_str("atoi1qprxpfvaz2peggq6f8k9cj8zfsxuw69e4nszjyv5kuf8yt70t2847shpjak").unwrap(),
336        );
337        let state_controller = QueryParameter::StateController(
338            Bech32Address::try_from_str("atoi1qzt0nhsf38nh6rs4p6zs5knqp6psgha9wsv74uajqgjmwc75ugupx3y7x0r").unwrap(),
339        );
340
341        let mut query_parameters = QueryParameters::new([address1, address2, state_controller]);
342        // since address1 and address2 are of the same enum variant, we should only have one
343        assert!(query_parameters.0.len() == 2);
344        // since address2 and address3 are of the same enum variant, we should only have one
345        query_parameters.replace(address3);
346        assert!(query_parameters.0.len() == 2);
347        // Contains address query parameter
348        assert!(query_parameters.any(|param| matches!(param, QueryParameter::Address(_))));
349        // Contains no cursor query parameter
350        assert!(!query_parameters.any(|param| matches!(param, QueryParameter::Cursor(_))));
351    }
352}