stellar_rs/accounts/
accounts_request.rs

1use crate::{models::*, BuildQueryParametersExt};
2
3/// Defines types for filtering the list of accounts retrieved.
4///
5/// This module provides a set of filter types used in [`AccountsRequest`]
6/// to specify the criteria for filtering the list of accounts returned by the Horizon server.
7/// Each filter type corresponds to a potential query parameter that can be used in account-
8/// related queries. Exactly one filter is required by the API in the request.
9///
10/// # Usage
11/// To use these filters, create an instance of [`AccountsRequest`]
12/// and call one of its setter methods to set exactly one of the filters. The request can then be executed through the `HorizonClient`.
13///
14/// ```rust
15/// # use stellar_rs::accounts::prelude::*;
16/// # use stellar_rs::accounts::accounts_request::filters::*;
17/// # use stellar_rs::models::Request;
18/// # use stellar_rs::horizon_client::HorizonClient;
19/// #
20/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
21/// # let base_url = "https://horizon-testnet.stellar.org";
22/// # let horizon_client = HorizonClient::new(base_url)
23/// #    .expect("Failed to create Horizon Client");
24/// let request = AccountsRequest::new()
25///     .set_signer_filter("GDQJUTQYK2MQX2VGDR2FYWLIYAQIEGXTQVTFEMGH2BEWFG4BRUY4CKI7")
26///     .unwrap();
27///
28/// // Use with HorizonClient::get_account_list
29/// # Ok({})
30/// # }
31/// ```
32///
33/// These filter types are designed to be used exclusively with `AccountsRequest` and are not intended
34/// for direct use in API calls.
35///
36pub mod filters {
37    use crate::models::Asset;
38
39    /// Represents a filter for accounts sponsored by the given account ID.
40    #[derive(Default, Clone)]
41    pub struct SponsorFilter(pub String);
42    /// Indicates the absence of a sponsor filter in the request.
43    #[derive(Default, Clone)]
44    pub struct NoSponsorFilter;
45
46    /// Represents a filter for accounts that have the given account ID as a signer.
47    #[derive(Default, Clone)]
48    pub struct SignerFilter(pub String);
49    /// Indicates the absence of a signer filter in the request.
50    #[derive(Default, Clone)]
51    pub struct NoSignerFilter;
52
53    /// Represents a filter for accounts holding a trustline for the specified asset.
54    #[derive(Clone)]
55    pub struct AssetFilter<T>(pub Asset<T>);
56    /// Indicates the absence of an asset filter in the request.
57    #[derive(Default, Clone)]
58    pub struct NoAssetFilter;
59
60    /// Represents a filter for accounts associated with the specified liquidity pool.
61    #[derive(Default, Clone)]
62    pub struct LiquidityPoolFilter(pub String);
63    /// Indicates the absence of a liquidity pool filter in the request.
64    #[derive(Default, Clone)]
65    pub struct NoLiquidityPoolFilter;
66}
67
68use filters::*;
69
70/// Macro to implement the `Request` trait for `AccountsRequest` variants.
71///
72/// This macro generates an implementation of the [`Request`] trait for a specified [`AccountsRequest`] type.
73/// It's utilized to create specific request handlers for different account query filters such as by sponsor,
74/// signer, asset, or liquidity pool.
75///
76/// # Parameters
77/// - `$type`: The type of [`AccountsRequest`] for which to implement the [`Request`] trait. This type must already
78///   conform to the structure expected by the Horizon API for account requests.
79/// - `$field`: The field within the `$type` that is being used as a filter for the account request. This field
80///   is included as a mandatory parameter in the query.
81///
82/// # Provided Methods
83/// - `get_query_parameters`: Constructs the query string from the fields of the `$type`, including cursor, limit,
84///   order, and the specified `$field` as a filter parameter.
85/// - `build_url`: Assembles the complete URL for the account request using the base URL and the constructed query
86///   parameters.
87///
88/// # Note
89/// - The macro is intended for internal SDK use and contributes to the modularity of the account request system.
90/// - The `.$field.0` syntax assumes that the filter field within the [`AccountsRequest`] type is a tuple struct with
91///   the actual filter value as its first item.
92/// - The macro includes error handling to ensure that only the appropriate fields are included in the query parameters.
93///
94macro_rules! valid_account_request_impl {
95    ($type:ty, $field:ident) => {
96        impl Request for $type {
97            fn get_query_parameters(&self) -> String {
98                let mut params = vec![
99                    self.cursor.as_ref().map(|c| format!("cursor={}", c)),
100                    self.limit.as_ref().map(|l| format!("limit={}", l)),
101                    self.order.as_ref().map(|o| format!("order={}", o)),
102                ];
103
104                params.push(Some(format!("{}={}", stringify!($field), self.$field.0)));
105
106                params.build_query_parameters()
107            }
108
109            fn build_url(&self, base_url: &str) -> String {
110                format!(
111                    "{}/{}{}",
112                    base_url,
113                    super::ACCOUNTS_PATH,
114                    self.get_query_parameters()
115                )
116            }
117        }
118    };
119}
120
121/// Macro to implement the `Request` trait for generic `AccountsRequest` variants.
122///
123/// This macro generates an implementation of the [`Request`] trait for a specified [`AccountsRequest`] type.
124/// It's utilized to create specific request handlers for different account query filters such as by sponsor,
125/// signer, asset, or liquidity pool.
126///
127/// # Parameters
128/// - `$type`: The type of [`AccountsRequest`] for which to implement the [`Request`] trait. This type must already
129///   conform to the structure expected by the Horizon API for account requests.
130/// - `$field`: The field within the `$type` that is being used as a filter for the account request. This field
131///   is included as a mandatory parameter in the query.
132/// - `$generic` : The generic type used for the [`AssetFilter`] when querying accounts.
133///
134/// # Provided Methods
135/// - `get_query_parameters`: Constructs the query string from the fields of the `$type`, including cursor, limit,
136///   order, and the specified `$field` as a filter parameter.
137/// - `build_url`: Assembles the complete URL for the account request using the base URL and the constructed query
138///   parameters.
139///
140/// # Note
141/// - The macro is intended for internal SDK use and contributes to the modularity of the account request system.
142/// - The `.$field.0` syntax assumes that the filter field within the [`AccountsRequest`] type is a tuple struct with
143///   the actual filter value as its first item.
144/// - The macro includes error handling to ensure that only the appropriate fields are included in the query parameters.
145///
146macro_rules! valid_generic_account_request_impl {
147    ($type:ty, $field:ident, $generic:ident) => {
148        impl<$generic> Request for $type
149        where
150            Asset<T>: std::fmt::Display,
151        {
152            fn get_query_parameters(&self) -> String {
153                let mut params = vec![
154                    self.cursor.as_ref().map(|c| format!("cursor={}", c)),
155                    self.limit.as_ref().map(|l| format!("limit={}", l)),
156                    self.order.as_ref().map(|o| format!("order={}", o)),
157                ];
158
159                params.push(Some(format!("{}={}", stringify!($field), self.$field.0)));
160
161                params.build_query_parameters()
162            }
163
164            fn build_url(&self, base_url: &str) -> String {
165                format!(
166                    "{}/{}{}",
167                    base_url,
168                    super::ACCOUNTS_PATH,
169                    self.get_query_parameters()
170                )
171            }
172        }
173    };
174}
175
176/// Specifies the requirements for a valid account request.
177///
178/// This trait ensures that any request structure intended to fetch account data from the
179/// Horizon server satisfies the necessary interface defined by the `Request` trait. It serves as
180/// a marker trait that categorically identifies valid account request types.
181///
182/// # Implementations
183/// The trait is implemented by various configurations of the `AccountsRequest` struct, each tailored
184/// to filter the account data based on different criteria:
185/// - `AccountsRequest<Sponsor, NoSigner, NoAsset, NoLiquidityPool>`: Requests accounts by sponsor.
186/// - `AccountsRequest<NoSponsor, Signer, NoAsset, NoLiquidityPool>`: Requests accounts by signer.
187/// - `AccountsRequest<NoSponsor, NoSigner, Asset, NoLiquidityPool>`: Requests accounts by asset.
188/// - `AccountsRequest<NoSponsor, NoSigner, NoAsset, LiquidityPool>`: Requests accounts by liquidity pool.
189///
190/// # Usage
191/// You generally do not need to use `ValidAccountsRequest` directly; it is used internally by the SDK.
192/// Instead, create an instance of [`AccountsRequest`] with the desired filters and pass it to the
193/// [`HorizonClient::get_account_list`](crate::horizon_client::HorizonClient::get_account_list) method.
194///
195/// ```rust
196/// # use stellar_rs::accounts::prelude::AccountsRequest;
197/// # use stellar_rs::horizon_client::HorizonClient;
198/// # use stellar_rs::models::Request;
199/// # let horizon_client = HorizonClient::new("https://horizon-testnet.stellar.org").unwrap();
200/// let request = AccountsRequest::new()
201///     .set_sponsor_filter("GDQJUTQYK2MQX2VGDR2FYWLIYAQIEGXTQVTFEMGH2BEWFG4BRUY4CKI7")
202///     .unwrap();
203/// // Now, you can pass `request` to `horizon_client.get_account_list`.
204/// ```
205///
206pub trait ValidAccountsRequest: Request {}
207
208impl ValidAccountsRequest
209    for AccountsRequest<SponsorFilter, NoSignerFilter, NoAssetFilter, NoLiquidityPoolFilter>
210{
211}
212valid_account_request_impl!(AccountsRequest<SponsorFilter, NoSignerFilter, NoAssetFilter, NoLiquidityPoolFilter>, sponsor);
213
214impl ValidAccountsRequest
215    for AccountsRequest<NoSponsorFilter, SignerFilter, NoAssetFilter, NoLiquidityPoolFilter>
216{
217}
218valid_account_request_impl!(AccountsRequest<NoSponsorFilter, SignerFilter, NoAssetFilter, NoLiquidityPoolFilter>, signer);
219
220impl<T> ValidAccountsRequest
221    for AccountsRequest<NoSponsorFilter, NoSignerFilter, AssetFilter<T>, NoLiquidityPoolFilter>
222where
223    Asset<T>: std::fmt::Display,
224{
225}
226valid_generic_account_request_impl!(AccountsRequest<NoSponsorFilter, NoSignerFilter, AssetFilter<T>, NoLiquidityPoolFilter>, asset, T);
227
228impl ValidAccountsRequest
229    for AccountsRequest<NoSponsorFilter, NoSignerFilter, NoAssetFilter, LiquidityPoolFilter>
230{
231}
232valid_account_request_impl!(AccountsRequest<NoSponsorFilter, NoSignerFilter, NoAssetFilter, LiquidityPoolFilter>, liquidity_pool);
233
234/// Represents a request to fetch multiple accounts from the Horizon API with a specific filter.
235///
236/// `AccountsRequest` is a struct used to query a list of accounts on the Horizon API, allowing
237/// filtering based on various criteria such as sponsor, signer, asset or liquidity pool.
238/// This struct is designed to be used in conjunction with the [`HorizonClient::get_account_list`](crate::horizon_client::HorizonClient::get_account_list) method.
239///
240/// The struct matches the parameters necessary to construct a request for the
241/// <a href="https://developers.stellar.org/api/horizon/resources/list-all-accounts">List All Accounts endpoint</a>
242/// of the Horizon API.
243///
244/// # Filters
245///
246/// At least one of the following filters is required:
247/// - `sponsor`: Account ID of the sponsor. Filters for accounts sponsored by the account ID or have a subentry (trustline, offer, or data entry) which is sponsored by the given account ID.
248/// - `signer`: Account ID of the signer. Filters for accounts that have the given account ID as a signer.
249/// - `asset`: An issued asset in the format “Code:IssuerAccountID”. Filters for accounts with a trustline for the specified asset.
250/// - `liquidity_pool`: The liquidity pool ID. Filters for accounts associated with the specified liquidity pool.
251///
252/// # Optional Parameters
253///
254/// - `cursor`: A number that points to the current location in the collection of responses and is pulled from the paging_token value of a record.
255/// - `limit`: The maximum number of records to return, with a permissible range from 1 to 200.
256///   Defaults to 10 if not specified.
257/// - `order`: The [`Order`] of the returned records, either ascending ([`Order::Asc`]) or descending ([`Order::Desc`]).
258///   Defaults to ascending if not set.
259///
260#[derive(Default)]
261pub struct AccountsRequest<Sp, Si, A, L> {
262    /// Filter for accounts sponsored by the account ID or have a subentry
263    /// (trustline, offer, or data entry) which is sponsored by the given account ID.
264    sponsor: Sp,
265
266    /// Filter for accounts that have the given account ID as a signer.
267    signer: Si,
268
269    /// Filter for accounts with a trustline for the specified asset.
270    asset: A,
271
272    /// Filter for accounts associated with the specified liquidity pool.
273    liquidity_pool: L,
274
275    /// A number that points to the current location in the collection of responses and is pulled from the paging_token value of a record.
276    cursor: Option<u32>,
277
278    /// The maximum number of records to return, with a permissible range from 1 to 200.
279    ///   Defaults to 10 if not specified.
280    limit: Option<u32>,
281
282    /// The [`Order`] of the returned records, either ascending ([`Order::Asc`]) or descending ([`Order::Desc`]).
283    order: Option<Order>,
284}
285
286impl<Sp, Si, A, L> AccountsRequest<Sp, Si, A, L> {
287    /// Sets the cursor for pagination.
288    ///
289    /// # Arguments
290    /// * `cursor` - A `u32` value pointing to a specific location in a collection of responses.
291    ///
292    pub fn set_cursor(self, cursor: u32) -> Result<Self, String> {
293        if cursor < 1 {
294            return Err("cursor must be greater than or equal to 1".to_string());
295        }
296
297        Ok(Self {
298            cursor: Some(cursor),
299            ..self
300        })
301    }
302
303    /// Sets the maximum number of records to return.
304    ///
305    /// # Arguments
306    /// * `limit` - A `u8` value specifying the maximum number of records. Range: 1 to 200. Defaults to 10.
307    ///
308    pub fn set_limit(self, limit: u32) -> Result<Self, String> {
309        if limit < 1 || limit > 200 {
310            return Err("limit must be between 1 and 200".to_string());
311        }
312
313        Ok(Self {
314            limit: Some(limit),
315            ..self
316        })
317    }
318
319    /// Sets the order of the returned records.
320    ///
321    /// # Arguments
322    /// * `order` - An [`Order`] enum value specifying the order (ascending or descending).
323    ///
324    pub fn set_order(self, order: Order) -> Self {
325        Self {
326            order: Some(order),
327            ..self
328        }
329    }
330}
331
332/// Since the Horizon API only allows for one of the following parameters to be set, we need to
333/// create an implementation for a combination of generics which are all unset.
334impl AccountsRequest<NoSponsorFilter, NoSignerFilter, NoAssetFilter, NoLiquidityPoolFilter> {
335    /// Creates a new `AccountsRequest` with default parameters.
336    pub fn new() -> Self {
337        AccountsRequest::default()
338    }
339
340    /// Sets the sponsor account ID filter.
341    ///
342    /// # Arguments
343    /// * `sponsor` - A `String` specifying the sponsor account ID. Filters for accounts
344    /// sponsored by this ID or having a subentry sponsored by this ID.
345    ///
346    pub fn set_sponsor_filter(
347        self,
348        sponsor: impl Into<String>
349    ) -> Result<
350        AccountsRequest<SponsorFilter, NoSignerFilter, NoAssetFilter, NoLiquidityPoolFilter>,
351        String,
352    > {
353        let sponsor = sponsor.into();
354        if let Err(e) = is_public_key(&sponsor) {
355            return Err(e.to_string());
356        }
357
358        Ok(AccountsRequest {
359            sponsor: SponsorFilter(sponsor.into()),
360            cursor: self.cursor,
361            limit: self.limit,
362            order: self.order,
363            ..Default::default()
364        })
365    }
366
367    /// Sets the signer account ID filter.
368    ///
369    /// # Arguments
370    /// * `signer` - A `String` specifying the signer account ID. Filters for accounts
371    /// having this ID as a signer.
372    ///
373    pub fn set_signer_filter(
374        self,
375        signer: &str,
376    ) -> Result<
377        AccountsRequest<NoSponsorFilter, SignerFilter, NoAssetFilter, NoLiquidityPoolFilter>,
378        String,
379    > {
380        if let Err(e) = is_public_key(&signer) {
381            return Err(e.to_string());
382        }
383
384        Ok(AccountsRequest {
385            signer: SignerFilter(signer.to_string()),
386            cursor: self.cursor,
387            limit: self.limit,
388            order: self.order,
389            ..Default::default()
390        })
391    }
392
393    /// Sets the asset filter.
394    ///
395    /// # Arguments
396    /// * `asset` - An [`Asset`] specifying the asset. Filters for accounts with a
397    /// trustline for this asset.
398    ///
399    pub fn set_asset_filter<T>(
400        self,
401        asset: Asset<T>,
402    ) -> AccountsRequest<NoSponsorFilter, NoSignerFilter, AssetFilter<T>, NoLiquidityPoolFilter>
403    {
404        AccountsRequest {
405            sponsor: self.sponsor,
406            signer: self.signer,
407            asset: AssetFilter(asset),
408            liquidity_pool: self.liquidity_pool,
409            cursor: self.cursor,
410            limit: self.limit,
411            order: self.order,
412        }
413    }
414
415    /// Sets the liquidity pool filter.
416    ///
417    /// # Arguments
418    /// * `liquidity_pool` - A `String` representing the liquidity pool ID. Filters for accounts
419    /// associated with the specified liquidity pool.
420    ///
421    pub fn set_liquidity_pool_filter(
422        self,
423        liquidity_pool: impl Into<String>,
424    ) -> AccountsRequest<NoSponsorFilter, NoSignerFilter, NoAssetFilter, LiquidityPoolFilter> {
425        AccountsRequest {
426            liquidity_pool: LiquidityPoolFilter(liquidity_pool.into()),
427            cursor: self.cursor,
428            limit: self.limit,
429            order: self.order,
430            ..Default::default()
431        }
432    }
433}
434
435#[cfg(test)]
436mod tests {
437    use super::*;
438
439    #[test]
440    fn test_accounts_request_set_sponsor_filter() {
441        let request = AccountsRequest::new().set_sponsor_filter("sponsor");
442
443        assert!(request.is_err());
444    }
445
446    #[test]
447    fn test_accounts_set_sponsor_valid() {
448        let request = AccountsRequest::new()
449            .set_sponsor_filter("GDQJUTQYK2MQX2VGDR2FYWLIYAQIEGXTQVTFEMGH2BEWFG4BRUY4CKI7")
450            .unwrap();
451        assert_eq!(
452            request.sponsor.0,
453            "GDQJUTQYK2MQX2VGDR2FYWLIYAQIEGXTQVTFEMGH2BEWFG4BRUY4CKI7"
454        );
455    }
456
457    #[test]
458    fn test_set_cursor_valid() {
459        let request = AccountsRequest::new().set_cursor(12345).unwrap();
460        assert_eq!(request.cursor.unwrap(), 12345);
461    }
462
463    #[test]
464    fn test_set_cursor_invalid() {
465        let request = AccountsRequest::new().set_cursor(0);
466        assert_eq!(
467            request.err().unwrap(),
468            "cursor must be greater than or equal to 1".to_string()
469        );
470    }
471
472    #[test]
473    fn test_set_limit_valid() {
474        let request = AccountsRequest::new().set_limit(20).unwrap();
475        assert_eq!(request.limit.unwrap(), 20);
476    }
477
478    #[test]
479    fn test_set_limit_invalid_low() {
480        let request = AccountsRequest::new().set_limit(0);
481        assert_eq!(
482            request.err().unwrap(),
483            "limit must be between 1 and 200".to_string()
484        );
485    }
486
487    #[test]
488    fn test_set_limit_invalid_high() {
489        let request = AccountsRequest::new().set_limit(201);
490        assert_eq!(
491            request.err().unwrap(),
492            "limit must be between 1 and 200".to_string()
493        );
494    }
495}