diff --git a/config.template.toml b/config.template.toml index e239e308..c633f4d2 100644 --- a/config.template.toml +++ b/config.template.toml @@ -49,6 +49,7 @@ api_key = "xxxxxx" [quests.nostra] utils_contract = "0xXXXXXXXXXXXX" pairs = ["0xXXXXXXXXXXXX"] +staking_contract="0xXXXXXXXXXXXX" [rhino] api_endpoint = "xxxxx" @@ -1045,4 +1046,42 @@ options = [ "LUSD 750", "LUSD 100" ] -correct_answers = [*] \ No newline at end of file +correct_answers = [*] + + +[quizzes.nostra2] +name = "Nostra Quiz" +desc = "Take part in our Quiz to test your knowledge about Nostra, and you'll have a chance to win 250 STRK." +intro = "Starknet Quest Quiz Rounds, a quiz series designed to make Starknet ecosystem knowledge accessible and enjoyable for all. Test your understanding of the workings of Nostra, enjoy the experience, and earn an exclusive NFT reward by testing your knowledge about Starknet Ecosystem projects!" + +[[quizzes.nostra2.questions]] +kind = "text_choice" +layout = "default" +question = "What is nstSTRK?" +options = [ + "The first liquid staking token on Starknet", + "A stablecoin on Nostra Money Market", + "A governance token for Nostra", +] +correct_answers = [*] + +[[quizzes.nostra2.questions]] +kind = "text_choice" +layout = "default" +question = "Will users be able to use their Nostra Staked STRK (nstSTRK) in DeFi protocols on Starknet?" +options = [ + "Yes - those who integrate with nstSTRK", + "No - it’s impossible", +] +correct_answers = [*] + +[[quizzes.nostra2.questions]] +kind = "text_choice" +layout = "default" +question = "What is the total value of idle STRK tokens on Starknet waiting to be staked on Nostra?" +options = [ + "$951 million", + "$100 million", + "$500 million", +] +correct_answers = [*] diff --git a/src/config.rs b/src/config.rs index 599cd33c..284a5bd4 100644 --- a/src/config.rs +++ b/src/config.rs @@ -36,6 +36,12 @@ pub_struct!(Clone, Deserialize; StarknetId { account_id: String, }); +pub_struct!(Clone, Deserialize; Nostra { + utils_contract: FieldElement, + pairs : Vec, + staking_contract: FieldElement, +}); + pub_struct!(Clone, Deserialize; Pairs { utils_contract: FieldElement, pairs : Vec, @@ -68,7 +74,7 @@ pub_struct!(Clone, Deserialize; Quests { myswap: Contract, braavos: Braavos, element: Element, - nostra: Pairs, + nostra: Nostra, carbonable: Contract, }); diff --git a/src/endpoints/get_quiz.rs b/src/endpoints/get_quiz.rs index 93ba7f44..272a0d1b 100644 --- a/src/endpoints/get_quiz.rs +++ b/src/endpoints/get_quiz.rs @@ -40,7 +40,6 @@ pub async fn handler( Query(query): Query, ) -> impl IntoResponse { let quizzes_from_config = &state.conf.quizzes; - match quizzes_from_config.get(&query.id) { Some(quiz) => { let questions: Vec = quiz diff --git a/src/endpoints/quests/nostra/claimable.rs b/src/endpoints/quests/nostra/liquidity_quest/claimable.rs similarity index 97% rename from src/endpoints/quests/nostra/claimable.rs rename to src/endpoints/quests/nostra/liquidity_quest/claimable.rs index 3e9c16de..b1f51590 100644 --- a/src/endpoints/quests/nostra/claimable.rs +++ b/src/endpoints/quests/nostra/liquidity_quest/claimable.rs @@ -29,7 +29,7 @@ pub struct ClaimableQuery { #[route( get, "/quests/nostra/claimable", - crate::endpoints::quests::nostra::claimable + crate::endpoints::quests::nostra::liquidity_quest::claimable )] pub async fn handler( State(state): State>, diff --git a/src/endpoints/quests/nostra/discord_fw_callback.rs b/src/endpoints/quests/nostra/liquidity_quest/discord_fw_callback.rs similarity index 98% rename from src/endpoints/quests/nostra/discord_fw_callback.rs rename to src/endpoints/quests/nostra/liquidity_quest/discord_fw_callback.rs index b1c46b2a..e6c390cf 100644 --- a/src/endpoints/quests/nostra/discord_fw_callback.rs +++ b/src/endpoints/quests/nostra/liquidity_quest/discord_fw_callback.rs @@ -31,7 +31,8 @@ pub struct Guild { #[route( get, "/quests/nostra/discord_fw_callback", - crate::endpoints::quests::nostra::discord_fw_callback + crate::endpoints::quests::nostra::liquidity_quest::discord_fw_callback + )] pub async fn handler( State(state): State>, diff --git a/src/endpoints/quests/nostra/liquidity_quest/mod.rs b/src/endpoints/quests/nostra/liquidity_quest/mod.rs new file mode 100644 index 00000000..57eade9b --- /dev/null +++ b/src/endpoints/quests/nostra/liquidity_quest/mod.rs @@ -0,0 +1,3 @@ +pub mod claimable; +pub mod discord_fw_callback; +pub mod verify_added_liquidity; diff --git a/src/endpoints/quests/nostra/verify_added_liquidity.rs b/src/endpoints/quests/nostra/liquidity_quest/verify_added_liquidity.rs similarity index 95% rename from src/endpoints/quests/nostra/verify_added_liquidity.rs rename to src/endpoints/quests/nostra/liquidity_quest/verify_added_liquidity.rs index 9208fb21..9e4fade1 100644 --- a/src/endpoints/quests/nostra/verify_added_liquidity.rs +++ b/src/endpoints/quests/nostra/liquidity_quest/verify_added_liquidity.rs @@ -21,7 +21,8 @@ use starknet::{ #[route( get, "/quests/nostra/verify_added_liquidity", - crate::endpoints::quests::nostra::verify_added_liquidity + crate::endpoints::quests::nostra::liquidity_quest::verify_added_liquidity + )] pub async fn handler( State(state): State>, diff --git a/src/endpoints/quests/nostra/mod.rs b/src/endpoints/quests/nostra/mod.rs index 57eade9b..0de6c9b1 100644 --- a/src/endpoints/quests/nostra/mod.rs +++ b/src/endpoints/quests/nostra/mod.rs @@ -1,3 +1,2 @@ -pub mod claimable; -pub mod discord_fw_callback; -pub mod verify_added_liquidity; +pub mod liquidity_quest; +pub mod staking_quest; \ No newline at end of file diff --git a/src/endpoints/quests/nostra/staking_quest/claimable.rs b/src/endpoints/quests/nostra/staking_quest/claimable.rs new file mode 100644 index 00000000..deefbda0 --- /dev/null +++ b/src/endpoints/quests/nostra/staking_quest/claimable.rs @@ -0,0 +1,107 @@ +use crate::models::{AppState, CompletedTaskDocument, Reward, RewardResponse}; +use crate::utils::{get_error, get_nft}; +use axum::{ + extract::{Query, State}, + http::StatusCode, + response::IntoResponse, + Json, +}; +use axum_auto_routes::route; +use futures::StreamExt; +use mongodb::bson::doc; +use serde::Deserialize; +use starknet::{ + core::types::FieldElement, + signers::{LocalWallet, SigningKey}, +}; +use std::sync::Arc; + +const QUEST_ID: u32 = 27; +const TASK_IDS: &[u32] = &[132, 133, 134]; +const LAST_TASK: u32 = TASK_IDS[2]; +const NFT_LEVEL: u32 = 39; + +#[derive(Deserialize)] +pub struct ClaimableQuery { + addr: FieldElement, +} + +#[route( + get, + "/quests/nostra/staking_quest/claimable", + crate::endpoints::quests::nostra::staking_quest::claimable +)] +pub async fn handler( + State(state): State>, + Query(query): Query, +) -> impl IntoResponse { + let collection = state + .db + .collection::("completed_tasks"); + + let pipeline = vec![ + doc! { + "$match": { + "address": &query.addr.to_string(), + "task_id": { "$in": TASK_IDS }, + }, + }, + doc! { + "$lookup": { + "from": "tasks", + "localField": "task_id", + "foreignField": "id", + "as": "task", + }, + }, + doc! { + "$match": { + "task.quest_id": QUEST_ID, + }, + }, + doc! { + "$group": { + "_id": "$address", + "completed_tasks": { "$push": "$task_id" }, + }, + }, + doc! { + "$match": { + "completed_tasks": { "$all": TASK_IDS }, + }, + }, + ]; + + let completed_tasks = collection.aggregate(pipeline, None).await; + match completed_tasks { + Ok(mut tasks_cursor) => { + if tasks_cursor.next().await.is_none() { + return get_error("User hasn't completed all tasks".into()); + } + + let signer = LocalWallet::from(SigningKey::from_secret_scalar( + state.conf.nft_contract.private_key, + )); + + let mut rewards = vec![]; + + let Ok((token_id, sig)) = get_nft(QUEST_ID, LAST_TASK, &query.addr, NFT_LEVEL, &signer).await else { + return get_error("Signature failed".into()); + }; + + rewards.push(Reward { + task_id: LAST_TASK, + nft_contract: state.conf.nft_contract.address.clone(), + token_id: token_id.to_string(), + sig: (sig.r, sig.s), + }); + + if rewards.is_empty() { + get_error("No rewards found for this user".into()) + } else { + (StatusCode::OK, Json(RewardResponse { rewards })).into_response() + } + } + Err(_) => get_error("Error querying rewards".into()), + } +} diff --git a/src/endpoints/quests/nostra/staking_quest/mod.rs b/src/endpoints/quests/nostra/staking_quest/mod.rs new file mode 100644 index 00000000..934bddc0 --- /dev/null +++ b/src/endpoints/quests/nostra/staking_quest/mod.rs @@ -0,0 +1,3 @@ +pub mod claimable; +pub mod verify_twitter_tw; +pub mod verify_stake; diff --git a/src/endpoints/quests/nostra/staking_quest/verify_stake.rs b/src/endpoints/quests/nostra/staking_quest/verify_stake.rs new file mode 100644 index 00000000..dc19c53d --- /dev/null +++ b/src/endpoints/quests/nostra/staking_quest/verify_stake.rs @@ -0,0 +1,79 @@ +use std::sync::Arc; + +use crate::{ + models::{AppState, VerifyQuery}, + utils::{get_error, CompletedTasksTrait}, +}; +use axum::{ + extract::{Query, State}, + http::StatusCode, + response::IntoResponse, + Json, +}; +use axum_auto_routes::route; +use serde_json::json; +use starknet::{ + core::types::{BlockId, BlockTag, FieldElement, FunctionCall}, + macros::selector, + providers::Provider, +}; + +#[route( +get, +"/quests/nostra/staking_quest/verify_stake", +crate::endpoints::quests::nostra::staking_quest::verify_stake +)] +pub async fn handler( + State(state): State>, + Query(query): Query, +) -> impl IntoResponse { + let task_id = 133; + let addr = &query.addr; + let balance_calldata = vec![*addr]; + let balance_result = state + .provider + .call( + FunctionCall { + contract_address: state.conf.quests.nostra.staking_contract, + entry_point_selector: selector!("balance_of"), + calldata: balance_calldata, + }, + BlockId::Tag(BlockTag::Latest), + ) + .await; + + let user_balance = match &balance_result { + Ok(result) => result[0], + Err(e) => return get_error(format!("{}", e)), + }; + + if user_balance == FieldElement::ZERO { + return get_error("You didn't stake any STRK.".to_string()); + } + + let call_result = state + .provider + .call( + FunctionCall { + contract_address: state.conf.quests.nostra.staking_contract, + entry_point_selector: selector!("convert_to_assets"), + calldata: balance_result.unwrap().to_vec(), + }, + BlockId::Tag(BlockTag::Latest), + ) + .await; + + match call_result { + Ok(result) => { + if result[0] < FieldElement::from_dec_str("10").unwrap() { + get_error("You need to stake atleast 10 STRK".to_string()) + } else { + match state.upsert_completed_task(query.addr, task_id).await { + Ok(_) => (StatusCode::OK, Json(json!({"res": true}))).into_response(), + Err(e) => get_error(format!("{}", e)), + } + } + } + Err(e) => get_error(format!("{}", e)), + } +} diff --git a/src/endpoints/quests/nostra/staking_quest/verify_twitter_tw.rs b/src/endpoints/quests/nostra/staking_quest/verify_twitter_tw.rs new file mode 100644 index 00000000..93a8bad7 --- /dev/null +++ b/src/endpoints/quests/nostra/staking_quest/verify_twitter_tw.rs @@ -0,0 +1,30 @@ +use std::sync::Arc; + +use crate::{ + models::{AppState, VerifyQuery}, + utils::{get_error, CompletedTasksTrait}, +}; +use axum::{ + extract::{Query, State}, + http::StatusCode, + response::IntoResponse, + Json, +}; +use axum_auto_routes::route; +use serde_json::json; + +#[route( +get, +"/quests/nostra/staking_quest/verify_twitter_tw", +crate::endpoints::quests::nostra::staking_quest::verify_twitter_tw +)] +pub async fn handler( + State(state): State>, + Query(query): Query, +) -> impl IntoResponse { + let task_id = 134; + match state.upsert_completed_task(query.addr, task_id).await { + Ok(_) => (StatusCode::OK, Json(json!({"res": true}))).into_response(), + Err(e) => get_error(format!("{}", e)), + } +} diff --git a/src/endpoints/quests/uri.rs b/src/endpoints/quests/uri.rs index 49745c43..d3000098 100644 --- a/src/endpoints/quests/uri.rs +++ b/src/endpoints/quests/uri.rs @@ -424,6 +424,17 @@ pub async fn handler( }), ).into_response(), + Some(39) => ( + StatusCode::OK, + Json(TokenURI { + name: "Nostra - Mafia Boss Cigar NFT".into(), + description: "A Nostra - Mafia Boss Cigar NFT won for successfully finishing the Quest".into(), + image: format!("{}/nostra/cigar.webp", state.conf.variables.app_link), + attributes: None, + }), + ).into_response(), + + _ => get_error("Error, this level is not correct".into()), } } diff --git a/src/endpoints/quests/verify_quiz.rs b/src/endpoints/quests/verify_quiz.rs index 20ebf784..911a6d8c 100644 --- a/src/endpoints/quests/verify_quiz.rs +++ b/src/endpoints/quests/verify_quiz.rs @@ -33,6 +33,7 @@ fn get_task_id(quiz_name: &str) -> Option { "braavos" => Some(98), "rhino" => Some(100), "nimbora" => Some(89), + "nostra2" => Some(132), _ => None, } }