diff --git a/stellar_rust_sdk/src/assets/mod.rs b/stellar_rust_sdk/src/assets/mod.rs index f95b947..d3655ab 100644 --- a/stellar_rust_sdk/src/assets/mod.rs +++ b/stellar_rust_sdk/src/assets/mod.rs @@ -81,20 +81,20 @@ pub mod test { #[tokio::test] async fn test_get_all_assets() { static ASSET_TYPE: &str = "credit_alphanum4"; - static ASSET_CODE: &str = "004"; - static ASSET_ISSUER: &str = "GDJUV2K6YBYUTI6GAA6P6BKVBQQDIOBZYDV2YOWOZI35LLSVMO6J4K7B"; + static ASSET_CODE: &str = "001"; + static ASSET_ISSUER: &str = "GAIX6Y5CRIH7A67IWGM26J6KIVPNXBHDPUP6KNHYSKFJ6VWABJKZYMKA"; static NUM_ACCOUNTS: &u32 = &1; static NUM_CLAIMABLE_BALANCES: &u32 = &0; static NUM_LIQUIDITY_POOLS: &u32 = &0; - static AMOUNT: &str = "999.0000000"; + static AMOUNT: &str = "10.0000000"; static AUTHORIZED: &u32 = &1; static AUTHORIZED_TO_MAINTAIN_LIABILITIES: &u32 = &0; static UNAUTHORIZED: &u32 = &0; static CLAIMABLE_BALANCES_AMOUNT: &str = "0.0000000"; static LIQUIDITY_POOLS_AMOUNT: &str = "0.0000000"; static CONTRACTS_AMOUNT: &str = "0.0000000"; - static BALANCES_AUTHORIZED: &str = "999.0000000"; + static BALANCES_AUTHORIZED: &str = "10.0000000"; static BALANCES_UNAUTHORIZED: &str = "0.0000000"; static AUTH_REQUIRED: &bool = &false; static AUTH_REVOCABLE: &bool = &false; diff --git a/stellar_rust_sdk/src/horizon_client.rs b/stellar_rust_sdk/src/horizon_client.rs index 3461e67..e0492e8 100644 --- a/stellar_rust_sdk/src/horizon_client.rs +++ b/stellar_rust_sdk/src/horizon_client.rs @@ -33,6 +33,7 @@ use crate::{ details_request::{BuyingAsset, DetailsRequest, SellingAsset}, response::DetailsResponse, }, + trade_aggregations::prelude::*, transactions::prelude::*, trades::prelude::*, }; @@ -1140,6 +1141,52 @@ impl HorizonClient { self.get::(request).await } + /// Retrieves a list of trade aggregations from the Horizon server. + /// + /// This asynchronous method fetches a list of trade aggregations from the Horizon server. + /// It requires a [`TradeAggregationsRequest`] to specify the parameters for the trade aggregations request. + /// + /// # Arguments + /// * `request` - A reference to a [`TradeAggregationsRequest`] instance, containing the parameters for the order book details request. + /// + /// # Returns + /// + /// On successful execution, returns a `Result` containing a [`AllTradeAggregationsResponse`], which includes the list of order book details obtained from the Horizon server. + /// If the request fails, it returns an error within `Result`. + /// + /// # Usage + /// To use this method, create an instance of [`TradeAggregationsRequest`] and set any desired filters or parameters. + /// + /// ```rust + /// use stellar_rs::horizon_client::HorizonClient; + /// use stellar_rs::trade_aggregations::prelude::*; + /// use stellar_rs::models::Request; + /// + /// # async fn example() -> Result<(), Box> { + /// let horizon_client = HorizonClient::new("https://horizon-testnet.stellar.org".to_string())?; + /// + /// // Example: Fetching trade aggregations + /// let request = TradeAggregationsRequest::new() + /// .set_base_asset(AssetType::Native).unwrap() + /// .set_counter_asset(AssetType::Alphanumeric4(AssetData { + /// asset_code: "USDC".to_string(), + /// asset_issuer: "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5".to_string(), + /// })).unwrap() + /// .set_resolution(Resolution(ResolutionData::Duration604800000)).unwrap(); + /// let response = horizon_client.get_trade_aggregations(&request).await?; + /// + /// // Process the response... + /// # Ok(()) + /// # } + /// ``` + /// + pub async fn get_trade_aggregations( + &self, + request: &TradeAggregationsRequest, + ) -> Result { + self.get::(request).await + } + /// Retrieves a list of all trades from the Horizon server. /// /// This asynchronous method fetches a list of all trades from the Horizon server. diff --git a/stellar_rust_sdk/src/lib.rs b/stellar_rust_sdk/src/lib.rs index b902284..bfdf682 100644 --- a/stellar_rust_sdk/src/lib.rs +++ b/stellar_rust_sdk/src/lib.rs @@ -508,6 +508,47 @@ pub mod operations; /// pub mod order_book; +/// Provides `Request` and `Response` structs for retrieving trade aggregation details. +/// +/// The `trade_aggregations` module in the Stellar Horizon SDK includes structures and methods that facilitate +/// querying trade aggregations data from the Horizon server. +/// +/// # Usage +/// +/// This module is used to construct requests for trade aggregations related data and to parse the responses +/// received from the Horizon server. It includes request and response structures for querying +/// trade aggregations. +/// +/// # Example +/// +/// To use this module, you can create an instance of a request struct, such as `TradeAggregationsRequest`, +/// set any desired query parameters, and pass the request to the `HorizonClient`. The client will +/// then execute the request and return the corresponding response struct, like `AllTradeAggregationsResponse`. +/// +/// ```rust +/// use stellar_rs::horizon_client::HorizonClient; +/// use stellar_rs::trade_aggregations::prelude::*; +/// use stellar_rs::models::Request; +/// +/// # async fn example() -> Result<(), Box> { +/// let horizon_client = HorizonClient::new("https://horizon-testnet.stellar.org".to_string())?; +/// +/// // Example: Fetching trade aggregations +/// let request = TradeAggregationsRequest::new() +/// .set_base_asset(AssetType::Native).unwrap() +/// .set_counter_asset(AssetType::Alphanumeric4(AssetData { +/// asset_code: "USDC".to_string(), +/// asset_issuer: "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5".to_string(), +/// })).unwrap() +/// .set_resolution(Resolution(ResolutionData::Duration604800000)).unwrap(); +/// let response = horizon_client.get_trade_aggregations(&request).await?; +/// +/// // Process the response... +/// # Ok(()) +/// # } +/// ``` +pub mod trade_aggregations; + /// Provides `Request` and `Response` structs for retrieving transactions. /// /// This module provides a set of specialized request and response structures designed for diff --git a/stellar_rust_sdk/src/liquidity_pools/mod.rs b/stellar_rust_sdk/src/liquidity_pools/mod.rs index 301b499..a9361f5 100644 --- a/stellar_rust_sdk/src/liquidity_pools/mod.rs +++ b/stellar_rust_sdk/src/liquidity_pools/mod.rs @@ -94,10 +94,10 @@ async fn test_get_all_liquidity_pools() { const RSP_2_LIQUIDITY_POOL_TOTAL_TRUSTLINES: &str = "1"; const RSP_2_LIQUIDITY_POOL_TOTAL_SHARES: &str = "249.0000000"; const RSP_2_LIQUIDITY_POOL_RESERVE_ASSET_0: &str = "native"; - const RSP_2_LIQUIDITY_POOL_RESERVE_AMOUNT_0: &str = "249.0000000"; + const RSP_2_LIQUIDITY_POOL_RESERVE_AMOUNT_0: &str = "1249.0000000"; const RSP_2_LIQUIDITY_POOL_RESERVE_ASSET_1: &str = "FLUTTER:GCGTOQSNERFVVJ6Y7YZYDF3MTZIY63KIEFMKA26Q7YPV3AFYD2JSRNYN"; - const RSP_2_LIQUIDITY_POOL_RESERVE_AMOUNT_1: &str = "249.0000000"; + const RSP_2_LIQUIDITY_POOL_RESERVE_AMOUNT_1: &str = "49.7600322"; const RSP_3_LIQUIDITY_POOL_ID: &str = "0b3c88caa5aeada296646c1810893e3b04cba0426cff8ff6a63cf6f35cc7f5b3"; @@ -294,12 +294,12 @@ async fn test_get_single_liquidity_pool() { const LIQUIDITY_POOL_TOTAL_SHARES: &str = "249.0000000"; const LIQUIDITY_POOL_RESERVE_ASSET_0: &str = "native"; - const LIQUIDITY_POOL_RESERVE_AMOUNT_0: &str = "249.0000000"; + const LIQUIDITY_POOL_RESERVE_AMOUNT_0: &str = "1249.0000000"; const LIQUIDITY_POOL_RESERVE_ASSET_1: &str = "FLUTTER:GCGTOQSNERFVVJ6Y7YZYDF3MTZIY63KIEFMKA26Q7YPV3AFYD2JSRNYN"; - const LIQUIDITY_POOL_RESERVE_AMOUNT_1: &str = "249.0000000"; - const LIQUIDITY_POOL_LAST_MODIFIED_LEDGER: i64 = 258622; - const LIQUIDITY_POOL_LAST_MODIFIED_TIME: &str = "2024-06-27T14:27:46Z"; + const LIQUIDITY_POOL_RESERVE_AMOUNT_1: &str = "49.7600322"; + const LIQUIDITY_POOL_LAST_MODIFIED_LEDGER: i64 = 716080; + const LIQUIDITY_POOL_LAST_MODIFIED_TIME: &str = "2024-07-25T11:09:01Z"; let horizon_client = HorizonClient::new("https://horizon-testnet.stellar.org".to_string()).unwrap(); diff --git a/stellar_rust_sdk/src/models/mod.rs b/stellar_rust_sdk/src/models/mod.rs index 66c41aa..9c8f18b 100644 --- a/stellar_rust_sdk/src/models/mod.rs +++ b/stellar_rust_sdk/src/models/mod.rs @@ -217,7 +217,7 @@ impl std::fmt::Display for Asset { /// * `Asc` - Indicates ascending order. /// * `Desc` - Indicates descending order. /// -#[derive(PartialEq, Debug)] +#[derive(Clone, PartialEq, Debug)] pub enum Order { Asc, Desc, diff --git a/stellar_rust_sdk/src/order_book/mod.rs b/stellar_rust_sdk/src/order_book/mod.rs index 97ecf35..e893961 100644 --- a/stellar_rust_sdk/src/order_book/mod.rs +++ b/stellar_rust_sdk/src/order_book/mod.rs @@ -14,9 +14,9 @@ pub mod tests { use crate::horizon_client; use crate::order_book::prelude::{Asset, AssetType, DetailsRequest}; - const BIDS_N: &u32 = &2; - const BIDS_D: &u32 = &1; - const BIDS_PRICE: &str = "2.0000000"; + const BIDS_N: &u32 = &3; + const BIDS_D: &u32 = &2; + const BIDS_PRICE: &str = "1.5000000"; const ASKS_N: &u32 = &5; const ASKS_D: &u32 = &1; const ASKS_PRICE: &str = "5.0000000"; diff --git a/stellar_rust_sdk/src/trade_aggregations/mod.rs b/stellar_rust_sdk/src/trade_aggregations/mod.rs new file mode 100644 index 0000000..68eaf91 --- /dev/null +++ b/stellar_rust_sdk/src/trade_aggregations/mod.rs @@ -0,0 +1,250 @@ +/// Provides the `TradeAggregationsRequest`. +/// +/// This module provides the `TradeAggregationsRequest` struct, specifically designed for +/// constructing requests to query information about trade aggregations from the Horizon +/// server. It is tailored for use with the [`HorizonClient::get_trade_aggregations`](crate::horizon_client::HorizonClient::get_trade_aggregations) +/// method. +/// +pub mod trade_aggregations_request; + +/// Provides the response. +/// +/// This module defines structures representing the response from the Horizon API when querying +/// for trade aggregations. The structures are designed to deserialize the JSON response into Rust +/// objects, enabling straightforward access to various details of a single Stellar account. +/// +/// These structures are equipped with serialization capabilities to handle the JSON data from the +/// Horizon server and with getter methods for easy field access. +/// +pub mod response; + +/// The base path for trade aggregations related endpoints in the Horizon API. +/// +/// # Usage +/// This variable is intended to be used internally by the request-building logic +/// to ensure consistent and accurate path construction for trade aggregations related API calls. +/// +static TRADE_AGGREGATIONS_PATH: &str = "trade_aggregations"; + +/// The `prelude` module of the `trade aggregations` module. +/// +/// This module serves as a convenience for users of the Horizon Rust SDK, allowing for easy and +/// ergonomic import of the most commonly used items across various modules. It re-exports +/// key structs and traits from the sibling modules, simplifying access to these components +/// when using the library. +/// +/// By importing the contents of `prelude`, users can conveniently access the primary +/// functionalities of the trade aggregations related modules without needing to import each item +/// individually. +/// +/// # Contents +/// +/// The `prelude` includes the following re-exports: +/// +/// * From `trade_aggregations_request`: All items (e.g. `TradeAggregationsRequest`, `Resolution`, etc.). +/// * From `response`: All items (e.g. `AllTradeAggregationsResponse`, `TradeAggregationResponse`, etc.). +/// +/// # Example +/// ``` +/// # use crate::stellar_rs::models::*; +/// // Import the contents of the offers prelude +/// use stellar_rs::trade_aggregations::prelude::*; +/// +/// // Now you can directly use TradeAggregationsRequest. +/// let trade_aggregations_request = TradeAggregationsRequest::new(); +/// ``` +/// +pub mod prelude { + pub use super::trade_aggregations_request::*; + pub use super::response::*; +} + +#[cfg(test)] +pub mod test { + use crate::{trade_aggregations::prelude::*, horizon_client::HorizonClient}; + + // Request constants. + const BASE_ASSET_ACCOUNT: &str = "GBZXN7PIRZGNMHGA7MUUUF4GWPY5AYPV6LY4UV2GL6VJGIQRXFDNMADI"; + const BASE_ASSET_CODE: &str = "XETH"; + const COUNTER_ASSET_ACCOUNT: &str = "GBZXN7PIRZGNMHGA7MUUUF4GWPY5AYPV6LY4UV2GL6VJGIQRXFDNMADI"; + const COUNTER_ASSET_CODE: &str = "XUSD"; + + // Response constants. + const TIMESTAMP: &str = "1717632000000"; + const TRADE_COUNT: &str = "39"; + const BASE_VOLUME: &str = "66.7280000"; + const COUNTER_VOLUME: &str = "51.0800000"; + const AVG: &str = "0.7654957"; + const HIGH: &str = "10.0000000"; + const HIGH_N: &str = "10"; + const HIGH_D: &str = "1"; + const LOW: &str = "0.1000000"; + const LOW_N: &str = "1"; + const LOW_D: &str = "10"; + const OPEN: &str = "0.3000000"; + const OPEN_N: &str = "3"; + const OPEN_D: &str = "10"; + const CLOSE: &str = "10.0000000"; + const CLOSE_N: &str = "10"; + const CLOSE_D: &str = "1"; + + #[tokio::test] + async fn test_set_offset() { + // Create the base of a valid request which can be cloned by the individual tests. + let request = TradeAggregationsRequest::new() + .set_base_asset(AssetType::Alphanumeric4(AssetData{ + asset_issuer: BASE_ASSET_ACCOUNT.to_string(), + asset_code: BASE_ASSET_CODE.to_string()})) + .unwrap() + .set_counter_asset(AssetType::Alphanumeric4(AssetData{ + asset_issuer: COUNTER_ASSET_ACCOUNT.to_string(), + asset_code: COUNTER_ASSET_CODE.to_string()})) + .unwrap(); + + // Check if an error is returned when trying to set an offset, when the resolution is smaller than an hour. + let result = request + .clone() + .set_resolution(Resolution(ResolutionData::Duration60000)) + .unwrap() + .set_offset(60000); + assert!(result.is_err()); + assert_eq!(result.unwrap_err(), "Resolution must be greater than 1 hour when setting offset."); + + // Check if an error is returned when passing unwhole hours in milliseconds. + let result = request + .clone() + .set_resolution(Resolution(ResolutionData::Duration604800000)) + .unwrap() + .set_offset(3999999); + assert!(result.is_err()); + assert_eq!(result.unwrap_err(), "Offset must be in whole hours."); + + // Check if an error is returned if the offset is greater than the set resolution. + let result = request + .clone() + .set_resolution(Resolution(ResolutionData::Duration3600000)) // 1 hour + .unwrap() + .set_offset(7200000); // 2 hours + assert!(result.is_err()); + assert_eq!(result.unwrap_err(), "Offset must be smaller than the resolution."); + + // Check if an error is returned if the offset is greater than 24 hours. + let result = request + .clone() + .set_resolution(Resolution(ResolutionData::Duration604800000)) + .unwrap() + .set_offset(604800000); // 1 week + assert!(result.is_err()); + assert_eq!(result.unwrap_err(), "Offset must be smaller than 24 hours."); + } + + #[tokio::test] + async fn test_get_trade_aggregations() { + let horizon_client = + HorizonClient::new("https://horizon-testnet.stellar.org" + .to_string()) + .unwrap(); + + let trade_aggregations_request = TradeAggregationsRequest::new() + .set_base_asset(AssetType::Alphanumeric4(AssetData{ + asset_issuer: BASE_ASSET_ACCOUNT.to_string(), + asset_code: BASE_ASSET_CODE.to_string()})) + .unwrap() + .set_counter_asset(AssetType::Alphanumeric4(AssetData{ + asset_issuer: COUNTER_ASSET_ACCOUNT.to_string(), + asset_code: COUNTER_ASSET_CODE.to_string()})) + .unwrap() + .set_resolution(Resolution(ResolutionData::Duration604800000)) + .unwrap(); + + let trade_aggregations_response = horizon_client + .get_trade_aggregations(&trade_aggregations_request) + .await; + + // assert!(trade_aggregations_response.clone().is_ok()); + let binding = trade_aggregations_response.unwrap(); + + let response = &binding.embedded().records()[0]; + assert_eq!(response.timestamp(), TIMESTAMP); + assert_eq!(response.trade_count(), TRADE_COUNT); + assert_eq!(response.base_volume(), BASE_VOLUME); + assert_eq!(response.counter_volume(), COUNTER_VOLUME); + assert_eq!(response.avg(), AVG); + assert_eq!(response.high(), HIGH); + assert_eq!(response.high_ratio().numenator(), HIGH_N); + assert_eq!(response.high_ratio().denominator(), HIGH_D); + assert_eq!(response.low(), LOW); + assert_eq!(response.low_ratio().numenator(), LOW_N); + assert_eq!(response.low_ratio().denominator(), LOW_D); + assert_eq!(response.open(), OPEN); + assert_eq!(response.open_ratio().numenator(), OPEN_N); + assert_eq!(response.open_ratio().denominator(), OPEN_D); + assert_eq!(response.close(), CLOSE); + assert_eq!(response.close_ratio().numenator(), CLOSE_N); + assert_eq!(response.close_ratio().denominator(), CLOSE_D); + } + + #[tokio::test] + async fn test_asset_query_parameters() { + use crate::models::*; + // Test if different combinations of asset types result in a valid RESTful query. The `Native` asset, for example, + // has a different amount of parameters than the alphanumeric types. The separators should always be correct, whatever + // the combination. + + // Test 2 different, non-native, asset types. + let request = TradeAggregationsRequest::new() + .set_base_asset(AssetType::Alphanumeric4(AssetData{ + asset_issuer: "baseissuer".to_string(), + asset_code: "basecode".to_string()})) + .unwrap() + .set_counter_asset(AssetType::Alphanumeric12(AssetData{ + asset_issuer: "counterissuer".to_string(), + asset_code: "countercode".to_string()})) + .unwrap() + .set_resolution(Resolution(ResolutionData::Duration604800000)) + .unwrap(); + assert_eq!(request.get_query_parameters(), + "?base_asset_type=credit_alphanum4&base_asset_code=basecode&base_asset_issuer=baseissuer&counter_asset_type=credit_alphanum12&counter_asset_code=countercode&counter_asset_issuer=counterissuer&resolution=604800000" + ); + + // Test 1 native, 1 non-native asset type. + let request = TradeAggregationsRequest::new() + .set_counter_asset(AssetType::Native) + .unwrap() + .set_base_asset(AssetType::Alphanumeric12(AssetData{ + asset_issuer: "counterissuer".to_string(), + asset_code: "countercode".to_string()})) + .unwrap() + .set_resolution(Resolution(ResolutionData::Duration604800000)) + .unwrap(); + assert_eq!(request.get_query_parameters(), + "?base_asset_type=credit_alphanum12&base_asset_code=countercode&base_asset_issuer=counterissuer&counter_asset_type=native&resolution=604800000" + ); + + // Test 1 non-native, 1 native asset type. + let request = TradeAggregationsRequest::new() + .set_base_asset(AssetType::Alphanumeric4(AssetData{ + asset_issuer: "counterissuer".to_string(), + asset_code: "countercode".to_string()})) + .unwrap() + .set_resolution(Resolution(ResolutionData::Duration604800000)) + .unwrap() + .set_counter_asset(AssetType::Native) + .unwrap(); + assert_eq!(request.get_query_parameters(), + "?base_asset_type=credit_alphanum4&base_asset_code=countercode&base_asset_issuer=counterissuer&counter_asset_type=native&resolution=604800000" + ); + + // Test 2 non-native asset types. + let request = TradeAggregationsRequest::new() + .set_base_asset(AssetType::Native) + .unwrap() + .set_resolution(Resolution(ResolutionData::Duration604800000)) + .unwrap() + .set_counter_asset(AssetType::Native) + .unwrap(); + assert_eq!(request.get_query_parameters(), + "?base_asset_type=native&counter_asset_type=native&resolution=604800000" + ); + } +} \ No newline at end of file diff --git a/stellar_rust_sdk/src/trade_aggregations/response.rs b/stellar_rust_sdk/src/trade_aggregations/response.rs new file mode 100644 index 0000000..bcccf81 --- /dev/null +++ b/stellar_rust_sdk/src/trade_aggregations/response.rs @@ -0,0 +1,84 @@ +use crate::models::prelude::*; +use derive_getters::Getters; +use serde::{Deserialize, Serialize}; + +/// Represents the response for the trade aggregations query in the Horizon API. +/// +/// This struct defines the overall structure of the response for a trade aggregations query. +/// It includes navigational links and embedded results. +/// +#[derive(Debug, Clone, Serialize, Deserialize, Getters)] +pub struct AllTradeAggregationsResponse { + #[serde(rename = "_links")] + pub links: ResponseLinks, + #[serde(rename = "_embedded")] + pub embedded: Embedded, +} + +impl Response for AllTradeAggregationsResponse { + fn from_json(json: String) -> Result { + let response = serde_json::from_str(&json).map_err(|e| e.to_string())?; + + Ok(response) + } +} + +/// Represents the precise buy and sell ratio of the trade. +/// +/// This struct contains a numenator and a denominator, so that the trade ratio can be determined +/// in a precise manner. +/// +#[derive(Debug, Deserialize, Serialize, Clone, Getters)] +pub struct Ratio { + /// The numenator. + #[serde(rename = "n")] + numenator: String, + /// The denominator. + #[serde(rename = "d")] + denominator: String, +} + +/// Represents a single record in a `TradeAggregations` query in the Horizon API. +/// +/// This struct defines the overall structure of a record for a single trade aggregation. +/// It includes navigational links, a timestamp, a trade count, and additional data. +/// +#[derive(Debug, Deserialize, Serialize, Clone, Getters)] +pub struct TradeAggregationResponse { + // Start time for this trade aggregation. Represented as milliseconds since epoch. + timestamp: String, + // Total number of trades aggregated. + trade_count: String, + // Total volume of base asset. + base_volume: String, + // Total volume of counter asset. + counter_volume: String, + // Weighted average price of counter asset in terms of base asset. + avg: String, + // The highest price for this time period. + high: String, + // The highest price for this time period as a rational number. + #[serde(rename = "high_r")] + high_ratio: Ratio, + // The lowest price for this time period. + low: String, + // The lowest price for this time period as a rational number. + #[serde(rename = "low_r")] + low_ratio: Ratio, + // The price as seen on first trade aggregated. + open: String, + // The price as seen on first trade aggregated as a rational number. + #[serde(rename = "open_r")] + open_ratio: Ratio, + // The price as seen on last trade aggregated. + close: String, + // The price as seen on last trade aggregated as a rational number. + #[serde(rename = "close_r")] + close_ratio: Ratio, +} + +impl Response for TradeAggregationResponse { + fn from_json(json: String) -> Result { + serde_json::from_str(&json).map_err(|e| e.to_string()) + } +} \ No newline at end of file diff --git a/stellar_rust_sdk/src/trade_aggregations/trade_aggregations_request.rs b/stellar_rust_sdk/src/trade_aggregations/trade_aggregations_request.rs new file mode 100644 index 0000000..8f52c0a --- /dev/null +++ b/stellar_rust_sdk/src/trade_aggregations/trade_aggregations_request.rs @@ -0,0 +1,372 @@ +use crate::{models::*, BuildQueryParametersExt}; + +/// Represents the base asset. Contains an enum of one of the possible asset types. +#[derive(Clone, PartialEq, Debug)] +pub struct BaseAsset(AssetType); + +/// Represents the absence of a base asset. +#[derive(PartialEq, Debug)] +pub struct NoBaseAsset; + +/// Represents the counter asset. Contains an enum of one of the possible asset types. +#[derive(Clone,PartialEq, Debug)] +pub struct CounterAsset(AssetType); + +/// Represents the absence of a counter asset. +#[derive(PartialEq, Debug)] +pub struct NoCounterAsset; + +/// Contains the details of a non-native asset. +#[derive(Clone, PartialEq, Debug, Default)] +pub struct AssetData { + pub asset_code: String, + pub asset_issuer: String, +} + +/// Represents the asset type of an asset. +#[derive(Clone, PartialEq, Debug)] +pub enum AssetType { + /// A native asset_type type. It holds no value. + // #[default] + Native, + /// An alphanumeric 4 asset_type type. It holds an Asset struct with asset code and asset issuer. + Alphanumeric4(AssetData), + /// An alphanumeric 12 asset_type type. It holds an Asset struct with asset code and asset issuer. + Alphanumeric12(AssetData), +} + +/// Represents the absense of a resolution value. +#[derive(Default, Clone)] +pub struct NoResolution; + +/// Represents the resolution value. It can contain a [`ResolutionData`] enum type. +#[derive(PartialEq, Debug, Default, Clone)] +pub struct Resolution(pub ResolutionData); + +/// Represents the supported segment duration times in milliseconds. +#[derive(PartialEq, Debug, Default, Clone)] +pub enum ResolutionData { + #[default] + Duration60000, + Duration300000, + Duration900000, + Duration3600000, + Duration604800000, +} + +impl std::fmt::Display for ResolutionData { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + ResolutionData::Duration60000 => write!(f, "60000"), // 1 minute + ResolutionData::Duration300000 => write!(f, "300000"), // 5 minutes + ResolutionData::Duration900000 => write!(f, "900000"), // 15 minutes + ResolutionData::Duration3600000 => write!(f, "3600000"), // 1 day + ResolutionData::Duration604800000 => write!(f, "604800000"), // 1 week + } + } +} + +/// Represents a request to list trade aggregations from the Stellar Horizon API. +/// +/// This structure is used to construct a query to retrieve a comprehensive list of trade aggregations, which will be filtered +/// by the mandatory base asset, counter asset and resolution fields. Additional filters such as start time, end time, limit +/// and order can be set. It adheres to the structure and parameters required by the Horizon API for retrieving a +/// list of trade aggregations. +/// +/// # Usage +/// +/// Create an instance of this struct and set the desired query parameters to filter the list of trade aggregations. +/// Pass this request object to the [`HorizonClient::get_trade_aggregations`](crate::horizon_client::HorizonClient::get_trade_aggregations) +/// method to fetch the corresponding data from the Horizon API. +/// +/// # Example +/// ``` +/// use stellar_rs::{trade_aggregations::prelude::*, models::*, Paginatable}; +/// +/// let request = TradeAggregationsRequest::new() +/// .set_base_asset(AssetType::Native).unwrap() +/// .set_counter_asset(AssetType::Alphanumeric4(AssetData{ +/// asset_issuer: "GBZXN7PIRZGNMHGA7MUUUF4GWPY5AYPV6LY4UV2GL6VJGIQRXFDNMADI".to_string(), +/// asset_code: "XETH".to_string(), +/// })).unwrap() +/// .set_resolution(Resolution(ResolutionData::Duration604800000)).unwrap() +/// .set_limit(100).unwrap() // Optional limit for response records +/// .set_order(Order::Desc); // Optional order of records +/// +/// // Use with HorizonClient::get_trade_aggregations +/// ``` +/// +#[derive(Clone, Debug, PartialEq, Default)] +pub struct TradeAggregationsRequest { + /// The base asset of the trade aggregation. + pub base_asset: B, + /// The counter asset of the trade. + pub counter_asset: C, + /// The lower time boundary represented as milliseconds since epoch. Optional. + pub start_time: Option, + /// The upper time boundary represented as milliseconds since epoch. Optional. + pub end_time: Option, + /// The segment duration represented as milliseconds. It must contain one of the `ResolutionData` enum types. + pub resolution: R, + /// Sgments can be offset using this parameter. Expressed in milliseconds. Optional. + pub offset: Option, + /// Specifies the maximum number of records to be returned in a single response. + /// The range for this parameter is from 1 to 200. The default value is set to 10. + pub limit: Option, + /// Determines the [`Order`] of the records in the response. Valid options are [`Order::Asc`] (ascending) + /// and [`Order::Desc`] (descending). If not specified, it defaults to ascending. + pub order: Option, +} + +impl TradeAggregationsRequest { + /// Constructor with default values. + pub fn new() -> Self { + TradeAggregationsRequest { + base_asset: NoBaseAsset, + counter_asset: NoCounterAsset, + resolution: NoResolution, + start_time: None, + end_time: None, + offset: None, + limit: None, + order: None, + } + } +} + +impl TradeAggregationsRequest { + /// Specifies the base asset in the request. + /// + /// # Arguments + /// + /// * `base_asset` - The base asset type to filter the trades. It can be one of the following: + /// - `AssetType::Native` + /// - `AssetType::Alphanumeric4(AssetData)` + /// - `AssetType::Alphanumeric12(AssetData)` + /// + /// # Returns + /// + /// The updated `TradeAggregationsRequest` with the base asset set. + /// + pub fn set_base_asset( + self, + base_asset: AssetType, + ) -> Result, String> { + Ok(TradeAggregationsRequest { + base_asset: BaseAsset(base_asset), + counter_asset: self.counter_asset, + start_time: self.start_time, + end_time: self.end_time, + offset: self.offset, + resolution: self.resolution, + limit: self.limit, + order: self.order, + }) + } + + /// Specifies the counter asset in the request. + /// + /// # Arguments + /// + /// * `counter_asset` - The counter asset type to filter the trades. It can be one of the following: + /// - `AssetType::Native` + /// - `AssetType::Alphanumeric4(AssetData)` + /// - `AssetType::Alphanumeric12(AssetData)` + /// + /// # Returns + /// + /// The updated `TradeAggregationsRequest` with the counter asset set. + /// + pub fn set_counter_asset( + self, + counter_asset: AssetType, + ) -> Result, String> { + Ok(TradeAggregationsRequest { + base_asset: self.base_asset, + counter_asset: CounterAsset(counter_asset), + start_time: self.start_time, + end_time: self.end_time, + offset: self.offset, + resolution: self.resolution, + limit: self.limit, + order: self.order, + }) + } + + /// Specifies the resolution in the request. + /// + /// # Arguments + /// + /// * `resolution` - The segment duration represented as milliseconds. + /// + /// # Returns + /// + /// The updated `TradeAggregationsRequest` with the resolution set. + /// + pub fn set_resolution( + self, + resolution: Resolution, + ) -> Result, String> { + Ok(TradeAggregationsRequest { + base_asset: self.base_asset, + counter_asset: self.counter_asset, + start_time: self.start_time, + end_time: self.end_time, + offset: self.offset, + resolution, + limit: self.limit, + order: self.order, + }) + } + + /// Specifies the start time in the request. + /// + /// # Arguments + /// + /// * `start_time` - The lower time boundary represented as milliseconds since epoch. + /// + pub fn set_start_time(self, start_time: Option) -> Result { + Ok(Self { + start_time, + ..self + }) + } + + /// Specifies the end time in the request. + /// + /// # Arguments + /// + /// * `end_time` - The upper time boundary represented as milliseconds since epoch. + /// + pub fn set_end_time(self, end_time: Option) -> Result { + Ok(Self { + end_time, + ..self + }) + } + + /// Specifies the maximum number of records to be returned. + /// + /// # Arguments + /// + /// * `limit` - The maximum number of records. + /// + pub fn set_limit(self, limit: u8) -> Result { + // Validate limit if necessary + if !(1..=200).contains(&limit) { + Err("Limit must be between 1 and 200.".to_string()) + } else { + Ok(Self { limit: Some(limit), ..self }) + } + } + + /// Specifies the order of records in the record. + /// Valid options are [`Order::Asc`] (ascending) + /// and [`Order::Desc`] (descending). If not specified, it defaults to ascending. /// # Arguments + /// + /// * `order` - A variant of the [`Order`] enum. + /// + pub fn set_order(self, order: Order) -> Result { + // No validation required for setting the order in this context + Ok(Self { order: Some(order), ..self }) + } +} + +impl TradeAggregationsRequest { + /// Sets the `offset` field in the request. + /// + /// Can only be used if the resolution is greater than 1 hour. Offset value must be in whole hours, + /// smaller than the provided resolution, and smaller than 24 hours. These conditions are first + /// checked before setting the offset field of the struct. Can only be set if the `resolution` + /// field has been set. + /// + /// # Arguments + /// + /// * `offset` - The offset represented as milliseconds. Note: although the `offset` field in the + /// [`TradeAggregationsRequest`] struct is of the type `String`, the `offset` argument is + /// of the type `u64` as a part of the condition check. + /// + /// # Returns + /// + /// A `Result` containing either the updated `TradeAggregationsRequest` or an error. + /// + pub fn set_offset(self, offset: u64) -> Result { + const ONE_HOUR: &u64 = &360000; + const ONE_DAY: &u64 = &86400000; + let resolution = format!("{}", &self.resolution.0) + .parse::() + .unwrap(); + + let conditions = [ + (&resolution < ONE_HOUR, "Resolution must be greater than 1 hour when setting offset."), + (&offset % ONE_HOUR != 0, "Offset must be in whole hours."), + (&offset > &resolution, "Offset must be smaller than the resolution."), + (&offset > ONE_DAY, "Offset must be smaller than 24 hours."), + ]; + + for (condition, message) in conditions { + if condition { + return Err(message.to_string()) + } + } + + Ok(Self { + offset: Some(offset.to_string()), + ..self + }) + } +} + +impl Request for TradeAggregationsRequest { + fn get_query_parameters(&self) -> String { + let asset_parameters = + vec![&self.base_asset.0, &self.counter_asset.0] + .iter() + .enumerate() + .fold(Vec::new(), |mut parameters, (i, asset)| { + let asset_type_prefix = if i == 0 { "base_asset_type=" } // no `&` for `base_asset_type`, as the query begins with `?` + else { "&counter_asset_type=" }; + match asset { + AssetType::Native => parameters.push(format!("{}native", asset_type_prefix)), + AssetType::Alphanumeric4(asset_data) | AssetType::Alphanumeric12(asset_data) => { + let asset_type = match asset { + AssetType::Alphanumeric4(_) => "credit_alphanum4", + AssetType::Alphanumeric12(_) => "credit_alphanum12", + _ => "", // should not be reached + }; + let asset_issuer_prefix = if i == 0 { "&base_asset_issuer=" } else { "&counter_asset_issuer=" }; + let asset_code_prefix = if i == 0 { "&base_asset_code=" } else { "&counter_asset_code=" }; + parameters.push(format!( + "{}{}{}{}{}{}", + asset_type_prefix, asset_type, + asset_code_prefix, asset_data.asset_code, + asset_issuer_prefix, asset_data.asset_issuer + )); + } + } + parameters + }) + .join(""); + + vec![ + Some(asset_parameters), + Some(format!("resolution={}", self.resolution.0)), + self.start_time.as_ref().map(|s| format!("start_time={}", s)), + self.end_time.as_ref().map(|e| format!("end_time={}", e)), + self.limit.as_ref().map(|l| format!("limit={}", l)), + self.order.as_ref().map(|o| format!("order={}", o)), + ].build_query_parameters() + } + + fn build_url(&self, base_url: &str) -> String { + format!( + "{}/{}{}", + base_url, + super::TRADE_AGGREGATIONS_PATH, + self.get_query_parameters() + ) + } +} + + +