From 54093d11ff293fa1817eae507ea116cc7db8d1b2 Mon Sep 17 00:00:00 2001 From: Iris Date: Wed, 24 Jan 2024 15:56:30 +0100 Subject: [PATCH 1/5] feat: add nimbora quest --- src/endpoints/quests/mod.rs | 1 + src/endpoints/quests/nimbora/claimable.rs | 109 +++++++++++++ .../quests/nimbora/discord_fw_callback.rs | 154 ++++++++++++++++++ src/endpoints/quests/nimbora/mod.rs | 3 + .../quests/nimbora/verify_twitter_rt.rs | 30 ++++ src/endpoints/quests/uri.rs | 10 ++ src/endpoints/quests/verify_quiz.rs | 1 + 7 files changed, 308 insertions(+) create mode 100644 src/endpoints/quests/nimbora/claimable.rs create mode 100644 src/endpoints/quests/nimbora/discord_fw_callback.rs create mode 100644 src/endpoints/quests/nimbora/mod.rs create mode 100644 src/endpoints/quests/nimbora/verify_twitter_rt.rs diff --git a/src/endpoints/quests/mod.rs b/src/endpoints/quests/mod.rs index 761f54e0..7288584d 100644 --- a/src/endpoints/quests/mod.rs +++ b/src/endpoints/quests/mod.rs @@ -19,3 +19,4 @@ pub mod verify_quiz; pub mod zklend; pub mod rhino; pub mod rango; +pub mod nimbora; diff --git a/src/endpoints/quests/nimbora/claimable.rs b/src/endpoints/quests/nimbora/claimable.rs new file mode 100644 index 00000000..a01934b5 --- /dev/null +++ b/src/endpoints/quests/nimbora/claimable.rs @@ -0,0 +1,109 @@ +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 = 22; +const TASK_IDS: &[u32] = &[89, 90, 91]; +const LAST_TASK: u32 = TASK_IDS[2]; +const NFT_LEVEL: u32 = 35; + +#[derive(Deserialize)] +pub struct ClaimableQuery { + addr: FieldElement, +} + +#[route( + get, + "/quests/nimbora/claimable", + crate::endpoints::quests::nimbora::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/nimbora/discord_fw_callback.rs b/src/endpoints/quests/nimbora/discord_fw_callback.rs new file mode 100644 index 00000000..0d050547 --- /dev/null +++ b/src/endpoints/quests/nimbora/discord_fw_callback.rs @@ -0,0 +1,154 @@ +use std::sync::Arc; + +use crate::utils::CompletedTasksTrait; +use crate::{ + models::AppState, + utils::{get_error_redirect, success_redirect}, +}; +use axum::{ + extract::{Query, State}, + response::IntoResponse, +}; +use axum_auto_routes::route; +use mongodb::bson::doc; +use reqwest::header::AUTHORIZATION; +use serde::Deserialize; +use starknet::core::types::FieldElement; + +#[derive(Deserialize)] +pub struct TwitterOAuthCallbackQuery { + code: String, + state: FieldElement, +} + +#[derive(Deserialize, Debug)] +pub struct Guild { + id: String, + #[allow(dead_code)] + name: String, +} + +#[route( + get, + "/quests/nimbora/discord_fw_callback", + crate::endpoints::quests::nimbora::discord_fw_callback +)] +pub async fn handler( + State(state): State>, + Query(query): Query, +) -> impl IntoResponse { + let quest_id = 22; + let task_id = 90; + let guild_id = "1157580917917892640"; + let authorization_code = &query.code; + let error_redirect_uri = format!( + "{}/quest/{}?task_id={}&res=false", + state.conf.variables.app_link, quest_id, task_id + ); + + // Exchange the authorization code for an access token + let params = [ + ("client_id", &state.conf.discord.oauth2_clientid), + ("client_secret", &state.conf.discord.oauth2_secret), + ("code", &authorization_code.to_string()), + ( + "redirect_uri", + &format!( + "{}/quests/nimbora/discord_fw_callback", + state.conf.variables.api_link + ), + ), + ("grant_type", &"authorization_code".to_string()), + ]; + let access_token = match exchange_authorization_code(params).await { + Ok(token) => token, + Err(e) => { + return get_error_redirect( + error_redirect_uri, + format!("Failed to exchange authorization code: {}", e), + ); + } + }; + + // Get user guild information + let client = reqwest::Client::new(); + let response_result = client + .get("https://discord.com/api/users/@me/guilds") + .header(AUTHORIZATION, format!("Bearer {}", access_token)) + .send() + .await; + let response: Vec = match response_result { + Ok(response) => { + let json_result = response.json().await; + match json_result { + Ok(json) => json, + Err(e) => { + return get_error_redirect( + error_redirect_uri, + format!( + "Failed to get JSON response while fetching user info: {}", + e + ), + ); + } + } + } + Err(e) => { + return get_error_redirect( + error_redirect_uri, + format!("Failed to send request to get user info: {}", e), + ); + } + }; + + for guild in response { + if guild.id == guild_id { + print!("Checking guild: {:?}", guild); + match state.upsert_completed_task(query.state, task_id).await { + Ok(_) => { + let redirect_uri = format!( + "{}/quest/{}?task_id={}&res=true", + state.conf.variables.app_link, quest_id, task_id + ); + return success_redirect(redirect_uri); + } + Err(e) => return get_error_redirect(error_redirect_uri, format!("{}", e)), + } + } + } + + get_error_redirect( + error_redirect_uri, + "You're not part of Nimbora's Discord server".to_string(), + ) +} + +async fn exchange_authorization_code( + params: [(&str, &String); 5], +) -> Result> { + let client = reqwest::Client::new(); + let res = client + .post("https://discord.com/api/oauth2/token") + .form(¶ms) + .send() + .await?; + let json: serde_json::Value = res.json().await?; + match json["access_token"].as_str() { + Some(s) => Ok(s.to_string()), + None => { + println!( + "Failed to get 'access_token' from JSON response : {:?}", + json + ); + Err(Box::new(std::io::Error::new( + std::io::ErrorKind::Other, + format!( + "Failed to get 'access_token' from JSON response : {:?}", + json + ), + ))) + } + } +} + + diff --git a/src/endpoints/quests/nimbora/mod.rs b/src/endpoints/quests/nimbora/mod.rs new file mode 100644 index 00000000..a3ca8f9b --- /dev/null +++ b/src/endpoints/quests/nimbora/mod.rs @@ -0,0 +1,3 @@ +pub mod verify_twitter_rt; +pub mod discord_fw_callback; +pub mod claimable; \ No newline at end of file diff --git a/src/endpoints/quests/nimbora/verify_twitter_rt.rs b/src/endpoints/quests/nimbora/verify_twitter_rt.rs new file mode 100644 index 00000000..2e4d7e8a --- /dev/null +++ b/src/endpoints/quests/nimbora/verify_twitter_rt.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/nimbora/verify_twitter_rt", + crate::endpoints::quests::nimbora::verify_twitter_rt +)] +pub async fn handler( + State(state): State>, + Query(query): Query, +) -> impl IntoResponse { + let task_id = 91; + 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 d0f398af..4cb7ff19 100644 --- a/src/endpoints/quests/uri.rs +++ b/src/endpoints/quests/uri.rs @@ -384,6 +384,16 @@ pub async fn handler( }), ).into_response(), + Some(35) => ( + StatusCode::OK, + Json(TokenURI { + name: "Nimbora NFT name".into(), + description: "Nimbora NFT name".into(), + image: format!("{}/nimbora/nft.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 19f5d7c5..20ebf784 100644 --- a/src/endpoints/quests/verify_quiz.rs +++ b/src/endpoints/quests/verify_quiz.rs @@ -32,6 +32,7 @@ fn get_task_id(quiz_name: &str) -> Option { "rango" => Some(95), "braavos" => Some(98), "rhino" => Some(100), + "nimbora" => Some(89), _ => None, } } From 7c4765a1202342350e5890d2406e3891b8696b37 Mon Sep 17 00:00:00 2001 From: Iris Date: Wed, 24 Jan 2024 17:21:30 +0100 Subject: [PATCH 2/5] fix: briq api url --- src/endpoints/achievements/verify_briq.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/endpoints/achievements/verify_briq.rs b/src/endpoints/achievements/verify_briq.rs index 06a30f49..8a71f192 100644 --- a/src/endpoints/achievements/verify_briq.rs +++ b/src/endpoints/achievements/verify_briq.rs @@ -46,7 +46,7 @@ pub async fn handler( Ok(Some(_)) => (StatusCode::OK, Json(json!({"achieved": true}))).into_response(), Ok(None) => { let url = format!( - "https://api.briq.construction/v1/user/data/starknet-mainnet/{}", + "https://api.briq.construction/v1/user/data/starknet-mainnet-dojo/{}", to_hex(addr) ); match fetch_json_from_url(url).await { @@ -57,7 +57,7 @@ pub async fn handler( for set in sets_array.iter() { if let serde_json::Value::String(set_str) = set { let url = format!( - "https://api.briq.construction/v1/metadata/starknet-mainnet/{}", + "https://api.briq.construction/v1/metadata/starknet-mainnet-dojo/{}", set_str ); match fetch_json_from_url(url).await { From 9ab050043d36b5d5e280cd5d11bff4e49a715ba6 Mon Sep 17 00:00:00 2001 From: Iris Date: Wed, 24 Jan 2024 17:22:08 +0100 Subject: [PATCH 3/5] fix: briq api url --- src/endpoints/quests/element/briq/verify_own_briq.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/endpoints/quests/element/briq/verify_own_briq.rs b/src/endpoints/quests/element/briq/verify_own_briq.rs index bdde2e5e..98bcb512 100644 --- a/src/endpoints/quests/element/briq/verify_own_briq.rs +++ b/src/endpoints/quests/element/briq/verify_own_briq.rs @@ -30,7 +30,7 @@ pub async fn handler( } let url = format!( - "https://api.briq.construction/v1/user/data/starknet-mainnet/{}", + "https://api.briq.construction/v1/user/data/starknet-mainnet-dojo/{}", to_hex(query.addr) ); match fetch_json_from_url(url).await { @@ -41,7 +41,7 @@ pub async fn handler( for set in sets_array.iter() { if let serde_json::Value::String(set_str) = set { let url = format!( - "https://api.briq.construction/v1/metadata/starknet-mainnet/{}", + "https://api.briq.construction/v1/metadata/starknet-mainnet-dojo/{}", set_str ); match fetch_json_from_url(url).await { From 01f9fe86d23ba216736912a9f5460abf2ca486fa Mon Sep 17 00:00:00 2001 From: Iris Date: Thu, 25 Jan 2024 15:42:44 +0100 Subject: [PATCH 4/5] ref: update nimbora nft name and desc --- src/endpoints/quests/uri.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/endpoints/quests/uri.rs b/src/endpoints/quests/uri.rs index 4cb7ff19..1115fb21 100644 --- a/src/endpoints/quests/uri.rs +++ b/src/endpoints/quests/uri.rs @@ -387,9 +387,9 @@ pub async fn handler( Some(35) => ( StatusCode::OK, Json(TokenURI { - name: "Nimbora NFT name".into(), - description: "Nimbora NFT name".into(), - image: format!("{}/nimbora/nft.webp", state.conf.variables.app_link), + name: "The Nimbora Pool".into(), + description: "A Nimbora NFT won for successfully finishing the Quest. Nimbora is bridging Ethereum's Layer 1 and Layer 2 seamlessly for cost-efficient DeFi interactions with improved user experience and uncompromised pooling.".into(), + image: format!("{}/nimbora/pool.webp", state.conf.variables.app_link), attributes: None, }), ).into_response(), From eee879165171008f9de71ffb88647cb377d3de60 Mon Sep 17 00:00:00 2001 From: Iris Date: Thu, 25 Jan 2024 15:49:19 +0100 Subject: [PATCH 5/5] feat: update config template file --- config.template.toml | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/config.template.toml b/config.template.toml index d687d827..64bf2ff6 100644 --- a/config.template.toml +++ b/config.template.toml @@ -1002,4 +1002,40 @@ options = [ "It means users can't recover their funds if the platform goes offline.", "It means users have to give up control of their funds to trade." ] +correct_answers = [*] + +[[quizzes.nimbora.questions]] +kind = "text_choice" +layout = "default" +question = "What is Nimbora?" +options = [ + "Liquidity mining", + "A traditional Layer 2 solution", + "Defi pooling solution bridging Ethereum's L1 and L2 for cost-efficient DeFi interactions", + "An L1 DeFi protocol without composability" +] +correct_answers = [*] + +[[quizzes.nimbora.questions]] +kind = "text_choice" +layout = "default" +question = "What is the benefit of Nimbora’s automatic withdrawal service?" +options = [ + "Manual claiming of tokens", + "Increased transaction fees", + "Hassle-free experience and minimized fees", + "Limited token selection" +] +correct_answers = [*] + +[[quizzes.nimbora.questions]] +kind = "text_choice" +layout = "default" +question = "What is the current maximum borrowing limit with Liquity?" +options = [ + "LUSD 1000", + "LUSD 500", + "LUSD 750", + "LUSD 100" +] correct_answers = [*] \ No newline at end of file