Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: nostra quest staking #201

Merged
merged 5 commits into from
Mar 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 40 additions & 1 deletion config.template.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ api_key = "xxxxxx"
[quests.nostra]
utils_contract = "0xXXXXXXXXXXXX"
pairs = ["0xXXXXXXXXXXXX"]
staking_contract="0xXXXXXXXXXXXX"

[rhino]
api_endpoint = "xxxxx"
Expand Down Expand Up @@ -1045,4 +1046,42 @@ options = [
"LUSD 750",
"LUSD 100"
]
correct_answers = [*]
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 = [*]
8 changes: 7 additions & 1 deletion src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ pub_struct!(Clone, Deserialize; StarknetId {
account_id: String,
});

pub_struct!(Clone, Deserialize; Nostra {
utils_contract: FieldElement,
pairs : Vec<FieldElement>,
staking_contract: FieldElement,
});

pub_struct!(Clone, Deserialize; Pairs {
utils_contract: FieldElement,
pairs : Vec<FieldElement>,
Expand Down Expand Up @@ -68,7 +74,7 @@ pub_struct!(Clone, Deserialize; Quests {
myswap: Contract,
braavos: Braavos,
element: Element,
nostra: Pairs,
nostra: Nostra,
carbonable: Contract,
});

Expand Down
1 change: 0 additions & 1 deletion src/endpoints/get_quiz.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ pub async fn handler(
Query(query): Query<GetQuizQuery>,
) -> impl IntoResponse {
let quizzes_from_config = &state.conf.quizzes;

match quizzes_from_config.get(&query.id) {
Some(quiz) => {
let questions: Vec<QuizQuestionResp> = quiz
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Arc<AppState>>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Arc<AppState>>,
Expand Down
3 changes: 3 additions & 0 deletions src/endpoints/quests/nostra/liquidity_quest/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pub mod claimable;
pub mod discord_fw_callback;
pub mod verify_added_liquidity;
Original file line number Diff line number Diff line change
Expand Up @@ -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<Arc<AppState>>,
Expand Down
5 changes: 2 additions & 3 deletions src/endpoints/quests/nostra/mod.rs
Original file line number Diff line number Diff line change
@@ -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;
107 changes: 107 additions & 0 deletions src/endpoints/quests/nostra/staking_quest/claimable.rs
Original file line number Diff line number Diff line change
@@ -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<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()),
}
}
3 changes: 3 additions & 0 deletions src/endpoints/quests/nostra/staking_quest/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pub mod claimable;
pub mod verify_twitter_tw;
pub mod verify_stake;
79 changes: 79 additions & 0 deletions src/endpoints/quests/nostra/staking_quest/verify_stake.rs
Original file line number Diff line number Diff line change
@@ -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<Arc<AppState>>,
Query(query): Query<VerifyQuery>,
) -> 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)),
}
}
30 changes: 30 additions & 0 deletions src/endpoints/quests/nostra/staking_quest/verify_twitter_tw.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/nostra/staking_quest/verify_twitter_tw",
crate::endpoints::quests::nostra::staking_quest::verify_twitter_tw
)]
pub async fn handler(
State(state): State<Arc<AppState>>,
Query(query): Query<VerifyQuery>,
) -> 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)),
}
}
11 changes: 11 additions & 0 deletions src/endpoints/quests/uri.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()),
}
}
1 change: 1 addition & 0 deletions src/endpoints/quests/verify_quiz.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ fn get_task_id(quiz_name: &str) -> Option<u32> {
"braavos" => Some(98),
"rhino" => Some(100),
"nimbora" => Some(89),
"nostra2" => Some(132),
_ => None,
}
}
Expand Down
Loading