diff --git a/Cargo.toml b/Cargo.toml index 2f6ad55..3bf8e7f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,10 @@ tokio = { version = "1.26.0", features = ["macros", "rt-multi-thread"] } tower-http = { version = "0.4.0", features = ["cors"] } mongodb = "2.4.0" futures = "0.3.28" -reqwest = { version = "0.11.17", features = ["json"] } +reqwest = { version = "0.12.0", features = ["rustls-tls", "json"] } +reqwest-middleware = { version = "0.3", features = ["json"] } +reqwest-retry = "0.5" +reqwest-tracing = "0.5" rand = "0.8.5" async-trait = "0.1.68" percent-encoding = "2.3.1" diff --git a/config.template.toml b/config.template.toml index f75ea94..ddf8f2f 100644 --- a/config.template.toml +++ b/config.template.toml @@ -83,6 +83,16 @@ contract = "0xFFFFFFFFFFFF" [quests.sithswap_2] api_endpoint = "xxxxxxx" +[rewards] +[rewards.nimbora] +contract = "0x07ed46700bd12bb1ee8a33a8594791003f9710a1ab18edd958aed86a8f82d3d1" + +[tokens] +[tokens.strk] +contract = "0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d" +symbol = "STRK" +decimals = 18 + [twitter] oauth2_clientid = "xxxxxx" oauth2_secret = "xxxxxx" diff --git a/src/config.rs b/src/config.rs index a6c927e..5e87b38 100644 --- a/src/config.rs +++ b/src/config.rs @@ -194,6 +194,20 @@ pub_struct!(Clone, Deserialize; ProtocolStats { alt_protocols_api_endpoint: String, }); +pub_struct!(Clone, Deserialize; Rewards { + nimbora: Contract, +}); + +pub_struct!(Clone, Deserialize; Token { + contract: FieldElement, + symbol: String, + decimals: i64 +}); + +pub_struct!(Clone, Deserialize; Tokens { + strk: Token, +}); + pub_struct!(Clone, Deserialize; Config { server: Server, database: Database, @@ -212,6 +226,8 @@ pub_struct!(Clone, Deserialize; Config { rango: Api, pyramid: ApiEndpoint, auth:AuthSetup, + rewards: Rewards, + tokens: Tokens }); pub fn load() -> Config { diff --git a/src/endpoints/defi/mod.rs b/src/endpoints/defi/mod.rs new file mode 100644 index 0000000..bad87da --- /dev/null +++ b/src/endpoints/defi/mod.rs @@ -0,0 +1 @@ +pub mod rewards; \ No newline at end of file diff --git a/src/endpoints/defi/rewards.rs b/src/endpoints/defi/rewards.rs new file mode 100644 index 0000000..131146e --- /dev/null +++ b/src/endpoints/defi/rewards.rs @@ -0,0 +1,365 @@ +use crate::{ + config::Config, + models::{ + AppState, CommonReward, ContractCall, DefiReward, EkuboRewards, NimboraRewards, + NostraPeriodsResponse, NostraResponse, RewardSource, ZkLendReward, + }, + utils::{check_if_claimed, to_hex_trimmed, to_hex}, +}; +use axum::{ + extract::{Query, State}, + http::StatusCode, + response::IntoResponse, + Json, +}; +use axum_auto_routes::route; +use futures::stream::{FuturesOrdered, StreamExt}; +use reqwest::header::{HeaderMap, HeaderValue, ACCEPT, USER_AGENT}; +use reqwest_middleware::{ClientBuilder, ClientWithMiddleware, Error}; +use reqwest_retry::{policies::ExponentialBackoff, RetryTransientMiddleware}; +use reqwest_tracing::TracingMiddleware; +use serde::{Deserialize, Serialize}; +use serde_json::json; +use starknet::{core::types::FieldElement, macros::selector}; +use std::{sync::Arc, vec}; + +#[derive(Debug, Serialize, Deserialize)] +pub struct RewardQuery { + addr: FieldElement, +} + +#[route(get, "/defi/rewards")] +pub async fn get_defi_rewards( + State(state): State>, + Query(query): Query, +) -> impl IntoResponse { + let addr = to_hex(query.addr); + + // Retry up to 3 times with increasing intervals between attempts. + let retry_policy = ExponentialBackoff::builder().build_with_max_retries(3); + let client = ClientBuilder::new(reqwest::Client::new()) + .with(TracingMiddleware::default()) + .with(RetryTransientMiddleware::new_with_policy(retry_policy)) + .build(); + + let (zklend_rewards, nostra_rewards, nimbora_rewards, ekubo_rewards) = tokio::join!( + fetch_zklend_rewards(&client, &addr), + fetch_nostra_rewards(&client, &addr, &state), + fetch_nimbora_rewards(&client, &addr, &state.conf), + fetch_ekubo_rewards(&client, &addr, &state), + ); + + let zklend_rewards = zklend_rewards.unwrap_or_default(); + let nostra_rewards = nostra_rewards.unwrap_or_default(); + let nimbora_rewards = nimbora_rewards.unwrap_or_default(); + let ekubo_rewards = ekubo_rewards.unwrap_or_default(); + + let all_rewards = [ + &zklend_rewards, + &nostra_rewards, + &nimbora_rewards, + &ekubo_rewards, + ]; + + let all_calls: Vec = all_rewards + .iter() + .flat_map(|rewards| create_calls(rewards, &addr)) + .collect(); + + let response_data = json!({ + "rewards": { + "zklend": extract_rewards(&zklend_rewards), + "nostra": extract_rewards(&nostra_rewards), + "nimbora": extract_rewards(&nimbora_rewards), + "ekubo": extract_rewards(&ekubo_rewards) + }, + "calls": all_calls + }); + + (StatusCode::OK, Json(response_data)).into_response() +} + +async fn fetch_zklend_rewards( + client: &ClientWithMiddleware, + addr: &str, +) -> Result, Error> { + let zklend_url = format!("https://app.zklend.com/api/reward/all/{}", addr); + let response = client + .get(&zklend_url) + .headers(get_headers()) + .send() + .await?; + + match response.json::>().await { + Ok(result) => { + let rewards = result + .into_iter() + .filter(|reward| !reward.claimed) + .map(|reward| CommonReward { + amount: reward.amount.value, + proof: reward.proof, + reward_id: Some(reward.claim_id), + claim_contract: reward.claim_contract, + token_symbol: reward.token.symbol, + reward_source: RewardSource::ZkLend, + claimed: reward.claimed, + }) + .collect(); + Ok(rewards) + } + Err(err) => { + eprintln!("Failed to deserialize zkLend response: {:?}", err); + Err(Error::Reqwest(err)) + } + } +} + +async fn fetch_nostra_rewards( + client: &ClientWithMiddleware, + addr: &str, + state: &AppState, +) -> Result, Error> { + let url = + "https://us-east-2.aws.data.mongodb-api.com/app/data-yqlpb/endpoint/data/v1/action/find"; + + let proof_request_body = json!({ + "dataSource": "nostra-production", + "database": "prod-a-nostra-db", + "collection": "rewardProofs", + "filter": { "account": addr } + }); + + let periods_request_body = json!({ + "dataSource": "nostra-production", + "database": "prod-a-nostra-db", + "collection": "rewardPeriods" + }); + + let (periods_resp, rewards_resp) = tokio::try_join!( + client + .post(url) + .headers(get_headers()) + .json(&periods_request_body) + .send(), + client + .post(url) + .headers(get_headers()) + .json(&proof_request_body) + .send() + )?; + + let reward_periods = match periods_resp.json::().await { + Ok(result) => result, + Err(err) => { + eprintln!("Failed to deserialize Nostra periods response: {:?}", err); + NostraPeriodsResponse { documents: vec![] } + } + }; + + let rewards = match rewards_resp.json::().await { + Ok(result) => result, + Err(err) => { + eprintln!("Failed to deserialize Nostra rewards response: {:?}", err); + return Err(Error::Reqwest(err)); + } + }; + + let addr_field = FieldElement::from_hex_be(addr).unwrap(); + let tasks: FuturesOrdered<_> = rewards + .documents + .into_iter() + .rev() + .map(|doc| { + let addr_field = addr_field.clone(); + let token_symbol = state.conf.tokens.strk.symbol.clone(); + let matching_period = reward_periods + .documents + .iter() + .find(|period| period.id == doc.reward_id && period.defi_spring_rewards); + + async move { + if let Some(distributor) = + matching_period.and_then(|period| period.defi_spring_rewards_distributor) + { + if check_if_claimed( + state, + distributor, + selector!("amount_already_claimed"), + vec![addr_field], + RewardSource::Nostra + ) + .await + { + Some(CommonReward { + amount: doc.reward, + proof: doc.proofs, + reward_id: None, + claim_contract: distributor, + token_symbol, + reward_source: RewardSource::Nostra, + claimed: false, + }) + } else { + None + } + } else { + None + } + } + }) + .collect(); + let active_rewards = tasks.filter_map(|res| async move { res }).collect().await; + Ok(active_rewards) +} + +// Fetch rewards from nimbora +async fn fetch_nimbora_rewards( + client: &ClientWithMiddleware, + addr: &str, + config: &Config, +) -> Result, Error> { + let nimbora_url = format!( + "https://strk-dist-backend.nimbora.io/get_calldata?address={}", + addr + ); + + let response = client + .get(&nimbora_url) + .headers(get_headers()) + .send() + .await?; + + let strk_symbol = config.tokens.strk.symbol.clone(); + + match response.json::().await { + Ok(result) => { + let reward = CommonReward { + amount: result.amount, + proof: result.proof, + reward_id: None, + token_symbol: strk_symbol.clone(), + claim_contract: config.rewards.nimbora.contract, + reward_source: RewardSource::Nimbora, + claimed: false, + }; + Ok(vec![reward]) + } + Err(err) => { + eprintln!("Failed to deserialize nimbora response: {:?}", err); + Err(Error::Reqwest(err)) + } + } +} + +async fn fetch_ekubo_rewards( + client: &ClientWithMiddleware, + addr: &str, + state: &AppState, +) -> Result, Error> { + let strk_token = state.conf.tokens.strk.clone(); + let ekubo_url = format!( + "https://mainnet-api.ekubo.org/airdrops/{}?token={}", + addr, + to_hex(strk_token.contract) + ); + + let response = client.get(&ekubo_url).headers(get_headers()).send().await?; + + let rewards = match response.json::>().await { + Ok(result) => result, + Err(err) => { + eprintln!("Failed to deserialize Ekubo rewards response: {:?}", err); + return Err(Error::Reqwest(err)); + } + }; + + let tasks: FuturesOrdered<_> = rewards + .into_iter() + .rev() + .map(|reward| { + let strk_token = strk_token.clone(); + async move { + if check_if_claimed( + state, + reward.contract_address, + selector!("is_claimed"), + vec![FieldElement::from(reward.claim.id)], + RewardSource::Ekubo + ) + .await + { + Some(CommonReward { + amount: reward.claim.amount, + proof: reward.proof, + reward_id: Some(reward.claim.id), + claim_contract: reward.contract_address, + token_symbol: strk_token.symbol, + reward_source: RewardSource::Ekubo, + claimed: false, + }) + } else { + None + } + } + }) + .collect(); + let active_rewards = tasks.filter_map(|res| async move { res }).collect().await; + Ok(active_rewards) +} + +fn create_calls(rewards: &[CommonReward], addr: &str) -> Vec { + rewards + .iter() + .filter(|reward| !reward.claimed) + .map(|reward| { + let calldata: Vec = match reward.reward_source { + RewardSource::ZkLend | RewardSource::Ekubo => { + let mut data = vec![ + to_hex_trimmed(FieldElement::from(reward.reward_id.unwrap())), + addr.to_string(), + to_hex_trimmed(reward.amount), + to_hex_trimmed(FieldElement::from(reward.proof.len())), + ]; + data.extend(reward.proof.clone()); + data + } + RewardSource::Nimbora | RewardSource::Nostra => { + let mut data = vec![ + to_hex_trimmed(reward.amount), + to_hex_trimmed(FieldElement::from(reward.proof.len())), + ]; + data.extend(reward.proof.clone()); + data + } + }; + + ContractCall { + contractaddress: to_hex(reward.claim_contract), + calldata, + entrypoint: "claim".to_string(), + } + }) + .collect() +} + +fn get_headers() -> HeaderMap { + let mut headers = HeaderMap::new(); + headers.insert(ACCEPT, HeaderValue::from_static("application/json")); + headers.insert( + USER_AGENT, + HeaderValue::from_static( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0", + ), + ); + headers +} + +fn extract_rewards(common_rewards: &[CommonReward]) -> Vec { + common_rewards + .iter() + .map(|reward| DefiReward { + amount: reward.amount, + token_symbol: reward.token_symbol.clone(), + }) + .collect() +} diff --git a/src/endpoints/get_boosted_quests.rs b/src/endpoints/get_boosted_quests.rs index 815b4bd..74b04cc 100644 --- a/src/endpoints/get_boosted_quests.rs +++ b/src/endpoints/get_boosted_quests.rs @@ -4,7 +4,7 @@ use axum::{extract::State, response::IntoResponse, Json}; use axum_auto_routes::route; use futures::TryStreamExt; use mongodb::bson::{doc, Document}; -use reqwest::StatusCode; +use axum::http::StatusCode; use std::sync::Arc; #[route(get, "/get_boosted_quests")] diff --git a/src/endpoints/get_completed_quests.rs b/src/endpoints/get_completed_quests.rs index f4756f0..2e39cac 100644 --- a/src/endpoints/get_completed_quests.rs +++ b/src/endpoints/get_completed_quests.rs @@ -8,7 +8,7 @@ use axum::{ use axum_auto_routes::route; use futures::TryStreamExt; use mongodb::bson::{doc, Document}; -use reqwest::StatusCode; +use axum::http::StatusCode; use serde::{Deserialize, Serialize}; use starknet::core::types::FieldElement; use std::sync::Arc; diff --git a/src/endpoints/get_quest_participants.rs b/src/endpoints/get_quest_participants.rs index 634d340..2e11b2e 100644 --- a/src/endpoints/get_quest_participants.rs +++ b/src/endpoints/get_quest_participants.rs @@ -8,7 +8,7 @@ use axum::{ use axum_auto_routes::route; use futures::StreamExt; use mongodb::bson::{doc, Document}; -use reqwest::StatusCode; +use axum::http::StatusCode; use serde::{Deserialize, Serialize}; use std::sync::Arc; diff --git a/src/endpoints/get_trending_quests.rs b/src/endpoints/get_trending_quests.rs index 0603119..6095702 100644 --- a/src/endpoints/get_trending_quests.rs +++ b/src/endpoints/get_trending_quests.rs @@ -10,7 +10,7 @@ use axum::{ use axum_auto_routes::route; use futures::StreamExt; use mongodb::bson::{doc, from_document}; -use reqwest::StatusCode; +use axum::http::StatusCode; use serde::{Deserialize, Serialize}; use starknet::core::types::FieldElement; use std::sync::Arc; diff --git a/src/endpoints/has_completed_quest.rs b/src/endpoints/has_completed_quest.rs index fc5cc00..e2beff8 100644 --- a/src/endpoints/has_completed_quest.rs +++ b/src/endpoints/has_completed_quest.rs @@ -8,7 +8,7 @@ use axum::{ use axum_auto_routes::route; use futures::TryStreamExt; use mongodb::bson::{doc, Document}; -use reqwest::StatusCode; +use axum::http::StatusCode; use serde::{Deserialize, Serialize}; use starknet::core::types::FieldElement; use std::sync::Arc; diff --git a/src/endpoints/leaderboard/get_ranking.rs b/src/endpoints/leaderboard/get_ranking.rs index 7a21dbe..2c0ea9d 100644 --- a/src/endpoints/leaderboard/get_ranking.rs +++ b/src/endpoints/leaderboard/get_ranking.rs @@ -46,7 +46,7 @@ use chrono::Utc; use futures::TryStreamExt; use mongodb::bson::{doc, Document}; use mongodb::Collection; -use reqwest::StatusCode; +use axum::http::StatusCode; use serde::{Deserialize, Serialize}; use std::sync::Arc; diff --git a/src/endpoints/leaderboard/get_static_info.rs b/src/endpoints/leaderboard/get_static_info.rs index 8e6fa5c..0a04f7f 100644 --- a/src/endpoints/leaderboard/get_static_info.rs +++ b/src/endpoints/leaderboard/get_static_info.rs @@ -20,7 +20,7 @@ use axum::response::Response; use chrono::Utc; use futures::TryStreamExt; use mongodb::bson::{doc, Document}; -use reqwest::StatusCode; +use axum::http::StatusCode; use serde::{Deserialize, Serialize}; use std::sync::Arc; diff --git a/src/endpoints/mod.rs b/src/endpoints/mod.rs index 4181eb6..71eedf8 100644 --- a/src/endpoints/mod.rs +++ b/src/endpoints/mod.rs @@ -16,4 +16,5 @@ pub mod has_completed_quest; pub mod leaderboard; pub mod quest_boost; pub mod quests; -pub mod unique_page_visit; \ No newline at end of file +pub mod unique_page_visit; +pub mod defi; \ No newline at end of file diff --git a/src/endpoints/quest_boost/get_claim_params.rs b/src/endpoints/quest_boost/get_claim_params.rs index c738b89..937aef2 100644 --- a/src/endpoints/quest_boost/get_claim_params.rs +++ b/src/endpoints/quest_boost/get_claim_params.rs @@ -9,7 +9,7 @@ use std::str::FromStr; use crate::utils::to_hex; use mongodb::bson::{doc, Bson, Document}; -use reqwest::StatusCode; +use axum::http::StatusCode; use serde::{Deserialize, Serialize}; use serde_json::json; use starknet::core::crypto::ecdsa_sign; diff --git a/src/endpoints/quest_boost/get_completed_boosts.rs b/src/endpoints/quest_boost/get_completed_boosts.rs index 62a9619..1c3b8da 100644 --- a/src/endpoints/quest_boost/get_completed_boosts.rs +++ b/src/endpoints/quest_boost/get_completed_boosts.rs @@ -8,7 +8,7 @@ use axum::{ use axum_auto_routes::route; use futures::TryStreamExt; use mongodb::bson::{doc, Document}; -use reqwest::StatusCode; +use axum::http::StatusCode; use serde::{Deserialize, Serialize}; use starknet::core::types::FieldElement; use std::sync::Arc; diff --git a/src/endpoints/quests/claimable.rs b/src/endpoints/quests/claimable.rs index 7550212..9059342 100644 --- a/src/endpoints/quests/claimable.rs +++ b/src/endpoints/quests/claimable.rs @@ -10,7 +10,7 @@ use crate::utils::get_nft; use axum_auto_routes::route; use futures::TryStreamExt; use mongodb::bson::{doc, Document}; -use reqwest::StatusCode; +use axum::http::StatusCode; use serde::{Deserialize, Serialize}; use starknet::core::types::FieldElement; use starknet::signers::{LocalWallet, SigningKey}; diff --git a/src/models.rs b/src/models.rs index 18662b7..88a7990 100644 --- a/src/models.rs +++ b/src/models.rs @@ -117,6 +117,13 @@ pub struct Call { pub regex: String, } +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct ContractCall { + pub contractaddress: String, + pub calldata: Vec, + pub entrypoint: String, +} + #[derive(Debug, Serialize, Deserialize, Default)] pub struct QuestTaskDocument { pub(crate) id: i32, @@ -367,3 +374,112 @@ pub_struct!(Deserialize; CreateBoostQuery { img_url: String, expiry: i64, }); + +#[derive(Serialize, Deserialize, Debug)] +pub struct ZkLendReward { + pub amount: Amount, + pub claim_contract: FieldElement, + pub claim_id: u64, + pub claimed: bool, + pub proof: Vec, + pub recipient: String, + pub token: Token, + #[serde(rename = "type")] + pub response_type: String, // renaming to avoid keyword conflict +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Amount { + pub decimals: u8, + pub value: FieldElement, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Token { + pub decimals: u8, + pub name: String, + pub symbol: String, +} +// Nostra Reward Structs +#[derive(Serialize, Deserialize, Debug)] +pub struct NostraResponse { + pub documents: Vec, // Array of reward documents +} + +// Nostra Reward Structs +#[derive(Serialize, Deserialize, Debug)] +pub struct NostraPeriodsResponse { + pub documents: Vec, // Array of reward documents +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct NostraReward { + #[serde(rename = "_id")] + pub id_internal: String, + pub id: String, + pub account: String, + pub proofs: Vec, + pub reward: FieldElement, + pub reward_id: String, +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct NostraRewardPeriods { + #[serde(rename = "_id")] + pub id_internal: String, + pub id: String, + pub defi_spring_rewards: bool, + pub defi_spring_rewards_distributor: Option, +} + +// Nimbora Reward Struct +#[derive(Serialize, Deserialize, Debug)] +pub struct NimboraRewards { + pub amount: FieldElement, + pub proof: Vec, +} + +// Ekubo Reward Structs +#[derive(Serialize, Deserialize, Debug)] +pub struct EkuboRewards { + pub contract_address: FieldElement, + pub token: String, + pub start_date: String, + pub end_date: String, + pub claim: Claim, + pub proof: Vec, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Claim { + pub id: u64, + pub amount: FieldElement, + pub claimee: String, +} + +#[derive(Serialize, Deserialize, Debug)] +pub enum RewardSource { + ZkLend, + Nostra, + Nimbora, + Ekubo, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct CommonReward { + pub amount: FieldElement, + pub proof: Vec, + pub reward_id: Option, + pub claim_contract: FieldElement, + pub token_symbol: String, + pub reward_source: RewardSource, + pub claimed: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct DefiReward { + pub amount: FieldElement, + pub token_symbol: String, +} diff --git a/src/utils.rs b/src/utils.rs index d83a579..d77155e 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,7 +1,6 @@ use crate::logger::Logger; use crate::models::{ - AchievementDocument, AppState, BoostTable, CompletedTasks, LeaderboardTable, QuestDocument, - QuestTaskDocument, UserExperience, + AchievementDocument, AppState, BoostTable, CompletedTasks, LeaderboardTable, QuestDocument, QuestTaskDocument, RewardSource, UserExperience }; use async_trait::async_trait; use axum::{ @@ -23,8 +22,9 @@ use starknet::signers::Signer; use starknet::{ core::{ crypto::{pedersen_hash, Signature}, - types::FieldElement, + types::{BlockId, BlockTag, FieldElement, FunctionCall}, }, + providers::{Provider, ProviderError}, signers::LocalWallet, }; use std::collections::hash_map::DefaultHasher; @@ -279,6 +279,20 @@ pub fn to_hex(felt: FieldElement) -> String { result } +pub fn to_hex_trimmed(felt: FieldElement) -> String { + let bytes = felt.to_bytes_be(); + let non_zero_index = bytes.iter().position(|&b| b != 0).unwrap_or(bytes.len()); + let mut result = String::from("0x"); + if non_zero_index == bytes.len() { + result.push('0'); + } else { + for &byte in &bytes[non_zero_index..] { + write!(&mut result, "{:02x}", byte).unwrap(); + } + } + result +} + #[async_trait] pub trait AchievementsTrait { async fn upsert_completed_achievement( @@ -837,10 +851,10 @@ pub fn parse_string(input: &str, address: FieldElement) -> String { let dec_address = address.to_string(); let regex_patterns = vec![ - (r"\{addr_hex\}", hex_address.as_str()), - (r"\{addr_dec\}", dec_address.as_str()), + (r"\{addr_hex\}", hex_address.as_str()), + (r"\{addr_dec\}", dec_address.as_str()), ]; - + for (pattern, replacement) in regex_patterns { let re = Regex::new(pattern).unwrap(); result = re.replace_all(&result, replacement).to_string(); @@ -869,3 +883,43 @@ pub async fn get_next_task_id( return (last_task_id as i32) + 1; } } + +pub async fn read_contract( + state: &AppState, + contract: FieldElement, + selector: FieldElement, + calldata: Vec, +) -> Result, ProviderError> { + state + .provider + .call( + FunctionCall { + contract_address: contract, + entry_point_selector: selector, + calldata, + }, + BlockId::Tag(BlockTag::Pending), + ) + .await +} + +pub async fn check_if_claimed( + state: &AppState, + contract: FieldElement, + selector: FieldElement, + calldata: Vec, + source: RewardSource +) -> bool { + match read_contract(state, contract, selector, calldata).await { + Ok(result) => result.get(0) == Some(&FieldElement::ZERO), + Err(err) => { + eprintln!( + "Error checking {:?} claim status: {:?} in {}", + source, + err, + to_hex(contract) + ); + false + } + } +} diff --git a/tests/endpoints.rs b/tests/endpoints.rs new file mode 100644 index 0000000..1ed2a89 --- /dev/null +++ b/tests/endpoints.rs @@ -0,0 +1,29 @@ +pub mod tests { + use reqwest::StatusCode; + + #[tokio::test] + pub async fn test_fail_without_address() { + let endpoint = format!("http://0.0.0.0:8080/defi/rewards"); + let client = reqwest::Client::new(); + let response = client.get(endpoint).send().await.unwrap(); + assert_eq!(response.status(), StatusCode::BAD_REQUEST); + } + + #[tokio::test] + pub async fn test_fail_with_invalid_address_format() { + let address = "0x03fbb5d22e1393e47ff967u88urui3u4iyr3ui4r90sduw0943jowefwruwerowu"; + let endpoint = format!("http://0.0.0.0:8080/defi/rewards?addr={}", address); + let client = reqwest::Client::new(); + let response = client.get(endpoint).send().await.unwrap(); + assert_eq!(response.status(), StatusCode::BAD_REQUEST); + } + + #[tokio::test] + pub async fn test_ok_with_valid_address_format() { + let address = "0x03fbb5d22e1393e47ff9678d12748885f176d8ce96051f72819cd2a6fa062589"; + let endpoint = format!("http://0.0.0.0:8080/defi/rewards?addr={}", address); + let client = reqwest::Client::new(); + let response = client.get(endpoint).send().await.unwrap(); + assert_eq!(response.status(), StatusCode::OK); + } +}