Skip to content

Commit

Permalink
Merge branch 'master' into feat/braavos_realms_quest
Browse files Browse the repository at this point in the history
  • Loading branch information
Th0rgal authored Jan 25, 2024
2 parents 4a22f78 + 2eb792d commit 59529df
Show file tree
Hide file tree
Showing 10 changed files with 348 additions and 4 deletions.
36 changes: 36 additions & 0 deletions config.template.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [*]
4 changes: 2 additions & 2 deletions src/endpoints/achievements/verify_briq.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down
4 changes: 2 additions & 2 deletions src/endpoints/quests/element/briq/verify_own_briq.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions src/endpoints/quests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ pub mod verify_quiz;
pub mod zklend;
pub mod rhino;
pub mod rango;
pub mod nimbora;
109 changes: 109 additions & 0 deletions src/endpoints/quests/nimbora/claimable.rs
Original file line number Diff line number Diff line change
@@ -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<Arc<AppState>>,
Query(query): Query<ClaimableQuery>,
) -> impl IntoResponse {
let collection = state
.db
.collection::<CompletedTaskDocument>("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()),
}
}
154 changes: 154 additions & 0 deletions src/endpoints/quests/nimbora/discord_fw_callback.rs
Original file line number Diff line number Diff line change
@@ -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<Arc<AppState>>,
Query(query): Query<TwitterOAuthCallbackQuery>,
) -> 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<Guild> = 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<String, Box<dyn std::error::Error>> {
let client = reqwest::Client::new();
let res = client
.post("https://discord.com/api/oauth2/token")
.form(&params)
.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
),
)))
}
}
}


3 changes: 3 additions & 0 deletions src/endpoints/quests/nimbora/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pub mod verify_twitter_rt;
pub mod discord_fw_callback;
pub mod claimable;
30 changes: 30 additions & 0 deletions src/endpoints/quests/nimbora/verify_twitter_rt.rs
Original file line number Diff line number Diff line change
@@ -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<Arc<AppState>>,
Query(query): Query<VerifyQuery>,
) -> 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)),
}
}
Loading

0 comments on commit 59529df

Please sign in to comment.