diff --git a/stellar_rust_sdk/src/horizon_client.rs b/stellar_rust_sdk/src/horizon_client.rs index df36b36..f29e86c 100644 --- a/stellar_rust_sdk/src/horizon_client.rs +++ b/stellar_rust_sdk/src/horizon_client.rs @@ -19,6 +19,7 @@ use crate::{ }, }, models::{Request, Response}, + offers::prelude::*, operations::{ operations_for_account_request::OperationsForAccountRequest, prelude::{ @@ -789,6 +790,60 @@ impl HorizonClient { self.get::(request).await } + /// Retrieves detailed information for a specific offer from the Horizon server. + /// + /// This asynchronous method fetches details of a single offer from the Horizon server. + /// It requires a [`SingleOfferRequest`] parameterized with `OfferId`, which includes the ID + /// of the offer to be retrieved. + /// + /// Adheres to the Retrieve An Offer endpoint + /// endpoint. + /// + /// # Arguments + /// + /// * `request` - A reference to a [`SingleOfferRequest`] instance, containing the + /// id of the offer for which details are to be fetched. + /// + /// # Returns + /// + /// Returns a `Result` containing an [`Offer`], which includes detailed + /// information about the requested offer. If the request fails, it returns an error + /// encapsulated within `Result`. + /// + /// # Usage + /// To use this method, create an instance of [`SingleOfferRequest`] and set the + /// id of the offer to be queried. + /// + /// ``` + /// # use stellar_rs::offers::prelude::*; + /// # use stellar_rs::models::Request; + /// # use stellar_rs::horizon_client::HorizonClient; + /// # + /// # async fn example() -> Result<(), Box> { + /// # let base_url = "https://horizon-testnet.stellar.org".to_string(); + /// # let horizon_client = HorizonClient::new(base_url) + /// # .expect("Failed to create Horizon Client"); + /// let request = SingleOfferRequest::new() + /// .set_offer_id("1".to_string()) // example offer ID + /// .unwrap(); + /// + /// let response = horizon_client.get_single_offer(&request).await; + /// + /// if let Ok(offer) = response { + /// println!("Offer ID: {}", offer.id()); + /// // Additional processing... + /// } + /// # Ok({}) + /// # } + /// ``` + /// + pub async fn get_single_offer( + &self, + request: &SingleOfferRequest, + ) -> Result { + self.get::(request).await + } + /// Retrieves a list of all operations from the Horizon server. /// /// This asynchronous method fetches a list of all operations from the Horizon server. diff --git a/stellar_rust_sdk/src/lib.rs b/stellar_rust_sdk/src/lib.rs index 117334c..446c6be 100644 --- a/stellar_rust_sdk/src/lib.rs +++ b/stellar_rust_sdk/src/lib.rs @@ -346,6 +346,48 @@ pub mod fee_stats; pub mod liquidity_pools; +/// Provides `Request` and `Response` structs for retrieving offers. +/// +/// This module provides a set of specialized request and response structures designed for +/// interacting with the offer-related endpoints of the Horizon server. These structures +/// facilitate the construction of requests to query offer data and the interpretation of +/// the corresponding responses. +/// +/// # Usage +/// +/// This module is intended to be used in conjunction with the [`HorizonClient`](crate::horizon_client::HorizonClient) +/// for making specific offer-related API calls to the Horizon server. The request +/// structures are designed to be passed to the client's methods, which handle the +/// communication with the server and return the corresponding response structures. +/// +/// # Example +/// +/// /// To use this module, you can create an instance of a request struct, such as +/// `SingleOfferRequest`, 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 `SingleOfferResponse`. +/// +/// ```rust +/// use stellar_rs::horizon_client::HorizonClient; +/// use stellar_rs::offers::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 all effects +/// let single_offer_request = SingleOfferRequest::new() +/// .set_offer_id("1".to_string()) +/// .unwrap(); +/// let single_offer_response = horizon_client.get_single_offer(&single_offer_request).await?; +/// +/// // Process the responses... +/// # Ok(()) +/// # } +/// ``` +/// +pub mod offers; + /// Provides `Request` and `Response` structs for retrieving operations. /// /// The `operations` module in the Stellar Horizon SDK includes structures and methods that facilitate diff --git a/stellar_rust_sdk/src/offers/mod.rs b/stellar_rust_sdk/src/offers/mod.rs new file mode 100644 index 0000000..b896bbc --- /dev/null +++ b/stellar_rust_sdk/src/offers/mod.rs @@ -0,0 +1,121 @@ +/// Provides the `SingleOfferRequest`. +/// +/// This module provides the `SingleOfferRequest` struct, specifically designed for +/// constructing requests to query information about a single offer from the Horizon +/// server. It is tailored for use with the [`HorizonClient::get_single_offer`](crate::horizon_client::HorizonClient::get_single_offer) +/// method. +/// +pub mod single_offer_request; + +/// Provides the responses. +/// +/// This module defines structures representing the response from the Horizon API when querying +/// for offers. 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 offer-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 offer-related API calls. +/// +static OFFERS_PATH: &str = "offers"; + +/// The `prelude` module of the `offers` 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 offer-related modules without needing to import each item +/// individually. +/// +/// # Contents +/// +/// The `prelude` includes the following re-exports: +/// +/// * From `single_offer_request`: All items (e.g. `SingleOfferRequest`). +/// * From `response`: All items (e.g. `SingleOfferResponse`, `PriceR`, etc.). +/// +/// # Example +/// ``` +/// # use crate::stellar_rs::models::*; +/// // Import the contents of the offers prelude +/// use stellar_rs::offers::prelude::*; +/// +/// // Now you can directly use SingleOfferRequest, SingleOfferResponse, etc. +/// let single_offer_request = SingleOfferRequest::new(); +/// ``` +/// +pub mod prelude { + pub use super::single_offer_request::*; + pub use super::response::*; +} + +#[cfg(test)] +pub mod test { + use super::prelude::*; + use crate::horizon_client::HorizonClient; + + #[tokio::test] + async fn test_get_single_offer() { + const LINK_SELF: &str = "https://horizon-testnet.stellar.org/offers/1"; + const LINK_OFFER_MAKER: &str = "https://horizon-testnet.stellar.org/accounts/GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"; + const OFFER_ID: &str = "1"; + const PAGING_TOKEN: &str = "1"; + const SELLER: &str = "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"; + const SELLING_ASSET_TYPE: &str = "credit_alphanum4"; + const SELLING_ASSET_CODE: &str = "USDC"; + const SELLING_ASSET_ISSUER: &str = "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"; + const BUYING_ASSET_TYPE: &str = "credit_alphanum12"; + const BUYING_ASSET_CODE: &str = "USDCAllow"; + const BUYING_ASSET_ISSUER: &str = "GAWZGWFOURKXZ4XYXBGFADZM4QIG6BJNM74XIZCEIU3BHM62RN2MDEZN"; + const AMOUNT: &str = "909086990804.0875807"; + const PRICE_R_N: &u32 = &1; + const PRICE_R_D: &u32 = &1; + const PRICE: &str = "1.0000000"; + const LAST_MODIFIED_LEDGER: &u32 = &747543; + const LAST_MODIFIED_TIME: &str = "2024-03-23T04:51:18Z"; + + let horizon_client = + HorizonClient::new("https://horizon-testnet.stellar.org" + .to_string()) + .unwrap(); + + let single_offer_request = + SingleOfferRequest::new() + .set_offer_id(OFFER_ID.to_string()) + .unwrap(); + + let single_offer_response = horizon_client + .get_single_offer(&single_offer_request) + .await; + + assert!(single_offer_response.clone().is_ok()); + let response = single_offer_response.unwrap(); + assert_eq!(response.links().self_link().href().as_ref().unwrap(), LINK_SELF); + assert_eq!(response.links().offer_maker().href().as_ref().unwrap(), LINK_OFFER_MAKER); + assert_eq!(response.id(), OFFER_ID); + assert_eq!(response.paging_token(), PAGING_TOKEN); + assert_eq!(response.seller(), SELLER); + assert_eq!(response.selling().asset_type(), SELLING_ASSET_TYPE); + assert_eq!(response.selling().asset_code().as_ref().unwrap(), SELLING_ASSET_CODE); + assert_eq!(response.selling().asset_issuer().as_ref().unwrap(), SELLING_ASSET_ISSUER); + assert_eq!(response.buying().asset_type(), BUYING_ASSET_TYPE); + assert_eq!(response.buying().asset_code().as_ref().unwrap(), BUYING_ASSET_CODE); + assert_eq!(response.buying().asset_issuer().as_ref().unwrap(), BUYING_ASSET_ISSUER); + assert_eq!(response.amount(), AMOUNT); + assert_eq!(response.price_ratio().numenator(), PRICE_R_N); + assert_eq!(response.price_ratio().denominator(), PRICE_R_D); + assert_eq!(response.price_decimal(), PRICE); + assert_eq!(response.last_modified_ledger(), LAST_MODIFIED_LEDGER); + assert_eq!(response.last_modified_time(), LAST_MODIFIED_TIME); + } +} \ No newline at end of file diff --git a/stellar_rust_sdk/src/offers/response.rs b/stellar_rust_sdk/src/offers/response.rs new file mode 100644 index 0000000..f136c3a --- /dev/null +++ b/stellar_rust_sdk/src/offers/response.rs @@ -0,0 +1,90 @@ +use crate::models::prelude::*; +use derive_getters::Getters; +use serde::{Deserialize, Serialize}; + +/// Represents the asset to buy or to sell. +/// +/// This struct details information about the asset to buy or to sell, including its type, +/// code (optional) and issuer (optional). +/// +#[derive(Debug, Deserialize, Clone, Getters)] +pub struct Transaction { + /// The type of asset (e.g. "credit_alphanum4", "credit_alphanum12"). + asset_type: String, + /// Optional. The code of the asset. + asset_code: Option, + /// Optional. The public key of the issuer. + asset_issuer: Option, +} + +/// Represents the precise buy and sell price of the assets on offer. +/// +/// This struct contains a numenator and a denominator, so that the price ratio can be determined +/// in a precise manner. +/// +#[derive(Debug, Deserialize, Clone, Getters)] +pub struct PriceR { + /// The numenator. + #[serde(rename = "n")] + numenator: u32, + /// The denominator. + #[serde(rename = "d")] + denominator: u32, +} + +/// Represents the navigational links in a single offer response from the Horizon API. +/// +/// This struct includes various hyperlinks such as links to the offer itself +/// and the offer maker. +/// +#[derive(Debug, Deserialize, Serialize, Clone, Getters)] +pub struct OfferResponseLinks { + /// The link to the offer itself. + #[serde(rename = "self")] + self_link: Link, + /// Link to the offer's maker. + offer_maker: Link, +} + +/// Represents the response for a single offer query in the Horizon API. +/// +/// This struct defines the overall structure of the response for a single offer query. +/// It includes navigational links, offer identifiers, the seller, the assets to buy and sell, +/// the amount, the price and additional data. +/// +#[derive(Debug, Deserialize, Clone, Getters)] +pub struct SingleOfferResponse { + /// Navigational links related to the offer. + #[serde(rename = "_links")] + links: OfferResponseLinks, + /// The unique identifier for the offer. + id: String, + /// A token used for paging through results. + paging_token: String, + /// The ID of the seller making the offer. + seller: String, + /// The asset the offer wants to sell. + selling: Transaction, + /// The asset the offer wants to buy. + buying: Transaction, + /// The amount of `selling` that the account making this offer is willing to sell. + amount: String, + /// A precise representation of the buy and sell price of the assets on offer. + #[serde(rename = "price_r")] + price_ratio: PriceR, + /// A number representing the decimal form of `price_r`. + #[serde(rename = "price")] + price_decimal: String, + /// The sequence number of the last ledger in which the offer was modified. + last_modified_ledger: u32, + /// The time at which the offer was last modified. + last_modified_time: String, + /// The account ID of the sponsor who is paying the reserves for this offer. + sponsor: Option, +} + +impl Response for SingleOfferResponse { + 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/offers/single_offer_request.rs b/stellar_rust_sdk/src/offers/single_offer_request.rs new file mode 100644 index 0000000..c575288 --- /dev/null +++ b/stellar_rust_sdk/src/offers/single_offer_request.rs @@ -0,0 +1,94 @@ +use crate::models::*; + +/// Represents the offer ID. +#[derive(Default, Clone)] +pub struct OfferId(String); + +/// Represents the absence of an offer ID. +#[derive(Default, Clone)] +pub struct NoOfferId; + +/// Represents a request to fetch details of an offer from the Horizon API. +/// +/// `SingleOfferRequest` is a struct tailored to querying details of a specific offer +/// on the Horizon API. This struct is designed to be used in conjunction with the +/// [`HorizonClient::get_single_offer`](crate::horizon_client::HorizonClient::get_single_offer) method. +/// +/// The struct matches the parameters necessary to construct a request for the +/// Retrieve An Offer endpoint +/// of the Horizon API. +/// +/// # Fields +/// Required: +/// * `offer_id` - The offer's ID. +/// +/// ## Usage +/// Instances of `SingleOfferRequest` are created and configured using setter methods for each +/// parameter. +/// ``` +/// # use stellar_rs::offers::prelude::SingleOfferRequest; +/// # use stellar_rs::models::Request; +/// let request = SingleOfferRequest::new() +/// .set_offer_id("1".to_string()); // example offer ID +/// +/// // Use with HorizonClient::get_single_offer +/// ``` +/// +#[derive(Default)] +pub struct SingleOfferRequest { + /// The ID of the offer to be retrieved. + offer_id: I, +} + +impl SingleOfferRequest { + /// Creates a new `SingleOfferRequest` with default parameters. + pub fn new() -> Self { + SingleOfferRequest::default() + } + + /// Sets the offer ID for the request. + /// + /// # Arguments + /// * `offer_id` - The offer ID to retrieve. + /// + /// # Returns + /// A `SingleOfferRequest` with the specified offer ID, or an error if the offer ID is invalid. + /// + pub fn set_offer_id( + self, + offer_id: String, + ) -> Result, String> { + match offer_id.parse::() { + Ok(id) => { + if id > 0 { + Ok(SingleOfferRequest { + offer_id: OfferId(offer_id) + }) + } else { + Err("offer ID must be greater than or equal to 1".to_string()) + } + } + Err(_) => Err("invalid offer ID".to_string()), + } + } +} + +impl Request for SingleOfferRequest { + fn get_query_parameters(&self) -> String { + let mut query = String::new(); + query.push_str(&format!("{}", self.offer_id.0)); + + query.trim_end_matches('&').to_string() + } + + fn build_url(&self, base_url: &str) -> String { + // This URL is not built with query paramaters, but with the offer ID as addition to the path. + // Therefore there is no `?` but a `/` in the formatted string. + format!( + "{}/{}/{}", + base_url, + super::OFFERS_PATH, + self.get_query_parameters() + ) + } +} \ No newline at end of file