From 75e4e153a269b4db83e9a83371fe073ffc35f8f5 Mon Sep 17 00:00:00 2001 From: LeonardTibben Date: Mon, 13 Nov 2023 15:32:01 +0100 Subject: [PATCH] claimable balances tests --- .../single_claimable_balance_request.rs | 91 +++++++++ .../single_claimable_balance_response.rs | 187 ++++++++++++++++++ src/horizon_client/horizon_client.rs | 137 ++++++++++++- 3 files changed, 411 insertions(+), 4 deletions(-) diff --git a/src/claimable_balances/single_claimable_balance_request.rs b/src/claimable_balances/single_claimable_balance_request.rs index e69de29..8d6605d 100644 --- a/src/claimable_balances/single_claimable_balance_request.rs +++ b/src/claimable_balances/single_claimable_balance_request.rs @@ -0,0 +1,91 @@ +use chrono::format; + +use crate::models::*; + +use super::super::AssetType; +use super::super::Order; + +/// SingleClaimableBalanceRequest is the struct that implements the type for the /claimable_balances endpoint to get a single claimable balance +/// [More Details](https://laboratory.stellar.org/#explorer?resource=claimable_balances&endpoint=single&network=test "Single Claimable Balance") +#[derive(Debug)] +pub struct SingleClaimableBalanceRequest { + /// Claimable Balance ID + /// [Stellar Documentation](https://developers.stellar.org/api/resources/claimablebalances/single/ "Claimable Balance ID") + claimable_balance_id: Option, +} + +impl Request for SingleClaimableBalanceRequest { + /// Creates a new request object + /// # Returns + /// A new request object + /// [SingleClaimableBalanceRequest](struct.SingleClaimableBalanceRequest.html) + fn new() -> Self { + SingleClaimableBalanceRequest { + claimable_balance_id: None, + } + } + + /// Gets the relative URL for the request + /// # Returns + /// The relative URL for the request + fn get_path(&self) -> &str { + "/claimable_balances/" + } + + /// Gets the query parameters for the request + /// # Returns + /// The query parameters for the request + fn get_query_parameters(&self) -> String { + let mut query = String::new(); + if let Some(claimable_balance_id) = &self.claimable_balance_id { + query.push_str(&format!("{}", claimable_balance_id)); + } + query + } + + /// Builds the URL for the request + /// # Arguments + /// * `self` - The request object + /// * `base_url` - The base URL for the Horizon server + /// # Returns + /// The URL for the request + fn build_url(&self, base_url: &str) -> String { + println!("\n\nBUILD URL: {:?}", format!( + "{}{}{}", + base_url, + self.get_path(), + self.get_query_parameters() + )); + + format!( + "{}{}{}", + base_url, + self.get_path(), + self.get_query_parameters() + ) + } + + /// Returns the type of request + /// # Returns + /// The type of request + /// [RequestType](../enum.RequestType.html) + /// [More Details](https://laboratory.stellar.org/#explorer?resource=claimable_balances&endpoint=single&network=test "Single Claimable Balance") + fn validate(&self) -> Result<(), String> { + + // TODO: Validate claimable_balance_id + + Ok(()) + } +} + +/// Returns the claimable balance ID +/// # Arguments +/// * `self` - The request object +/// # Returns +/// The claimable balance ID +impl SingleClaimableBalanceRequest { + pub fn set_claimable_balance_id(&mut self, claimable_balance_id: String) -> &mut Self { + self.claimable_balance_id = Some(claimable_balance_id); + self + } +} diff --git a/src/claimable_balances/single_claimable_balance_response.rs b/src/claimable_balances/single_claimable_balance_response.rs index e69de29..20c57ad 100644 --- a/src/claimable_balances/single_claimable_balance_response.rs +++ b/src/claimable_balances/single_claimable_balance_response.rs @@ -0,0 +1,187 @@ +use chrono::DateTime; +use chrono::Utc; +use derive_getters::Getters; +use serde::Deserialize; +use serde::Serialize; + +use crate::models::Response; + +#[derive(Default, Debug, Clone, Serialize, Deserialize, Getters)] +#[serde(rename_all = "camelCase")] +pub struct SingleClaimableBalanceResponse { + #[serde(rename = "_links")] + pub links: Links, + pub id: String, + pub asset: String, + pub amount: String, + pub sponsor: String, + #[serde(rename = "last_modified_ledger")] + pub last_modified_ledger: i64, + #[serde(rename = "last_modified_time")] + pub last_modified_time: String, + pub claimants: Vec, + pub flags: Flags, + #[serde(rename = "paging_token")] + pub paging_token: String, +} + +#[derive(Default, Debug, Clone, Serialize, Deserialize, Getters)] +#[serde(rename_all = "camelCase")] +pub struct Links { + #[serde(rename = "self")] + pub self_field: Self_field, + pub transactions: Transactions, + pub operations: Operations, +} + +#[derive(Default, Debug, Clone, Serialize, Deserialize, Getters)] +#[serde(rename_all = "camelCase")] +pub struct Self_field { + pub href: String, +} + +#[derive(Default, Debug, Clone, Serialize, Deserialize, Getters)] +#[serde(rename_all = "camelCase")] +pub struct Transactions { + pub href: String, + pub templated: bool, +} + +#[derive(Default, Debug, Clone, Serialize, Deserialize, Getters)] +#[serde(rename_all = "camelCase")] +pub struct Operations { + pub href: String, + pub templated: bool, +} + +#[derive(Default, Debug, Clone, Serialize, Deserialize, Getters)] +#[serde(rename_all = "camelCase")] +pub struct Claimant { + pub destination: String, + pub predicate: Predicate, +} + +#[derive(Default, Debug, Clone, Serialize, Deserialize, Getters)] +#[serde(rename_all = "camelCase")] +pub struct Predicate { + pub unconditional: Option, + pub or: Option>, +} + +#[derive(Default, Debug, Clone, Serialize, Deserialize, Getters)] +#[serde(rename_all = "camelCase")] +pub struct Or { + pub and: Option>, + #[serde(rename = "abs_before")] + pub abs_before: Option, + #[serde(rename = "abs_before_epoch")] + pub abs_before_epoch: Option, +} + +#[derive(Default, Debug, Clone, Serialize, Deserialize, Getters)] +#[serde(rename_all = "camelCase")] +pub struct And { + pub not: Option, + #[serde(rename = "abs_before")] + pub abs_before: Option, + #[serde(rename = "abs_before_epoch")] + pub abs_before_epoch: Option, +} + +#[derive(Default, Debug, Clone, Serialize, Deserialize, Getters)] +#[serde(rename_all = "camelCase")] +pub struct Not { + #[serde(rename = "abs_before")] + pub abs_before: String, + #[serde(rename = "abs_before_epoch")] + pub abs_before_epoch: String, +} + +#[derive(Default, Debug, Clone, Serialize, Deserialize, Getters)] +#[serde(rename_all = "camelCase")] +pub struct Flags { + #[serde(rename = "clawback_enabled")] + pub clawback_enabled: bool, +} + +impl Response for SingleClaimableBalanceResponse { + fn from_json(json: String) -> Result { + let response = serde_json::from_str(&json).map_err(|e| e.to_string())?; + + Ok(response) + } +} + +impl Predicate { + // This method checks if a claim is valid at a specific datetime. + pub fn is_valid_claim(&self, datetime: DateTime) -> bool { + // If the predicate is marked as unconditional, the claim is always valid. + if let Some(true) = self.unconditional { + true + } + // If there are 'or' conditions, check if any of these conditions validate the claim. + else if let Some(or_conditions) = &self.or { + or_conditions.iter().any(|or| or.is_valid(datetime)) + } + // If there are no conditions, the claim is valid. + else { + true + } + } +} + + +impl Or { + // This method checks if any condition under 'or' validates the claim. + fn is_valid(&self, datetime: DateTime) -> bool { + // If there are 'and' conditions, check if any combination of these conditions is valid. + if let Some(and_conditions) = &self.and { + and_conditions.iter().any(|and| and.is_valid(datetime)) + } + // If there is an 'abs_before' condition, check if the datetime is before this date. + else if let Some(abs_before) = &self.abs_before { + if let Ok(abs_before_date) = DateTime::parse_from_rfc3339(abs_before) { + datetime < abs_before_date + } else { + false + } + } + // If no specific condition is found, the claim is valid. + else { + true + } + } +} + +impl And { + // This method checks if all conditions under 'and' are met. + fn is_valid(&self, datetime: DateTime) -> bool { + let mut is_valid = true; + + // If there is an 'abs_before' condition, check if the datetime is before this date. + if let Some(abs_before) = &self.abs_before { + if let Ok(abs_before_date) = DateTime::parse_from_rfc3339(abs_before) { + is_valid &= datetime < abs_before_date; + } + } + + // If there is a 'not' condition, it should also validate the datetime. + if let Some(not_condition) = &self.not { + is_valid &= not_condition.is_valid(datetime); + } + + is_valid + } +} + +impl Not { + // This method checks if the datetime does not fall before the specified date, negating the condition. + fn is_valid(&self, datetime: DateTime) -> bool { + if let Ok(not_before_date) = DateTime::parse_from_rfc3339(&self.abs_before) { + datetime >= not_before_date + } else { + false + } + } +} + diff --git a/src/horizon_client/horizon_client.rs b/src/horizon_client/horizon_client.rs index 0c3e043..4416413 100644 --- a/src/horizon_client/horizon_client.rs +++ b/src/horizon_client/horizon_client.rs @@ -3,7 +3,10 @@ use crate::{ AccountsRequest, AccountsResponse, SingleAccountRequest, SingleAccountsResponse, }, assets::prelude::{AllAssetsRequest, AllAssetsResponse}, - claimable_balances::prelude::{AllClaimableBalancesRequest, AllClaimableBalancesResponse}, + claimable_balances::prelude::{ + AllClaimableBalancesRequest, AllClaimableBalancesResponse, SingleClaimableBalanceRequest, + SingleClaimableBalanceResponse, + }, ledgers::prelude::{ LedgersRequest, LedgersResponse, SingleLedgerRequest, SingleLedgerResponse, }, @@ -94,6 +97,22 @@ impl HorizonClient { self.get::(request).await } + /// Gets the base URL for the Horizon server + /// # Arguments + /// * `self` - The Horizon client + /// * request - The single claimable balance request + /// # Returns + /// The single claimable balance response + /// # Errors + /// Returns an error if the request fails + /// [GET /claimable_balances/{claimable_balance_id}](https://www.stellar.org/developers/horizon/reference/endpoints/claimable_balances-single.html) + pub async fn get_single_claimable_balance( + &self, + request: &SingleClaimableBalanceRequest, + ) -> Result { + self.get::(request).await + } + pub async fn get_all_ledgers( &self, request: &LedgersRequest, @@ -127,8 +146,9 @@ impl HorizonClient { // TODO: construct with query parameters let url = request.build_url(&self.base_url); - println!("\n\nURL: {}", url); + // println!("\n\nURL: {}", url); let response = reqwest::get(&url).await.map_err(|e| e.to_string())?; + println!("\n\nREQWEST RESPONSE: {:?}", response); let result: TResponse = handle_response(response).await?; // print!("\n\nResult: {:?}", result); @@ -150,7 +170,7 @@ async fn handle_response( match response.status() { reqwest::StatusCode::OK => { let _response = response.text().await.map_err(|e| e.to_string())?; - // println!("\n\nResponse: {:?}", _response); + // println!("\n\nHANDLE_RESPONSE RESPONSE: {:?}", _response); TResponse::from_json(_response) } _ => { @@ -161,11 +181,13 @@ async fn handle_response( } /// url_validate validates a URL fn url_validate(url: &str) -> Result<(), String> { + println!("URL: {}", url); // check if start with http:// or https:// if !url.starts_with("http://") && !url.starts_with("https://") { return Err(format!("URL must start with http:// or https://: {}", url)); } Url::parse(url).map_err(|e| e.to_string())?; + Ok(()) } @@ -174,7 +196,11 @@ mod tests { use base64::encode; use chrono::{DateTime, TimeZone, Utc}; - use crate::{assets::prelude::AllAssetsRequest, ledgers::prelude::SingleLedgerRequest}; + use crate::{ + assets::prelude::AllAssetsRequest, + claimable_balances::prelude::SingleClaimableBalanceRequest, + ledgers::prelude::SingleLedgerRequest, + }; use super::*; @@ -1372,4 +1398,107 @@ mod tests { false ); } + + #[tokio::test] + async fn test_get_single_claimable_balance() { + // Initialize horizon client + let horizon_client = + HorizonClient::new("https://horizon-testnet.stellar.org".to_string()).unwrap(); + + // construct request + let mut single_claimable_balance_request = SingleClaimableBalanceRequest::new(); + single_claimable_balance_request.set_claimable_balance_id( + "000000006520216af66d20d63a58534d6cbdf28ba9f2a9c1e03f8d9a756bb7d988b29bca".to_string(), + ); + + let single_claimable_balance_response = horizon_client + .get_single_claimable_balance(&single_claimable_balance_request) + .await; + + assert!(single_claimable_balance_response.is_ok()); + + let binding = single_claimable_balance_response.clone().unwrap(); + + let predicate = binding.claimants()[1].predicate(); + + let now = Utc::now(); + + let jan_first_2022 = Utc::with_ymd_and_hms(&Utc, 2022, 1, 1, 0, 0, 0).unwrap(); + + assert_eq!(predicate.is_valid_claim(now), true); + + assert_eq!(predicate.is_valid_claim(jan_first_2022), false); + + + assert_eq!( + single_claimable_balance_response + .clone() + .unwrap() + .id() + .to_string(), + "000000006520216af66d20d63a58534d6cbdf28ba9f2a9c1e03f8d9a756bb7d988b29bca" + ); + + assert_eq!( + single_claimable_balance_response + .clone() + .unwrap() + .asset() + .to_string(), + "native" + ); + + assert_eq!( + single_claimable_balance_response + .clone() + .unwrap() + .amount() + .to_string(), + "12.3300000" + ); + + assert_eq!( + single_claimable_balance_response + .clone() + .unwrap() + .sponsor() + .to_string(), + "GD7TMSN67PCPZ4SXQHNG4GFO4KEMGTAT6MGWQGKBPOFDY7TP2IYDYFVI" + ); + + assert_eq!( + *single_claimable_balance_response + .clone() + .unwrap() + .last_modified_ledger(), + 1560 + ); + + assert_eq!( + single_claimable_balance_response + .clone() + .unwrap() + .last_modified_time() + .to_string(), + "2023-06-14T11:38:24Z" + ); + + assert_eq!( + *single_claimable_balance_response + .clone() + .unwrap() + .flags() + .clawback_enabled(), + false + ); + + assert_eq!( + single_claimable_balance_response + .clone() + .unwrap() + .paging_token() + .to_string(), + "1560-000000006520216af66d20d63a58534d6cbdf28ba9f2a9c1e03f8d9a756bb7d988b29bca" + ); + } }