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}