Skip to content

Commit

Permalink
Merge pull request #88 from starknet-id/feat/add_braavos_quests
Browse files Browse the repository at this point in the history
feat: add braavos quests
  • Loading branch information
Th0rgal authored Oct 5, 2023
2 parents 93c1a0d + 6070bcd commit f27f28e
Show file tree
Hide file tree
Showing 29 changed files with 650 additions and 105 deletions.
5 changes: 3 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
starknet = { git = "https://github.com/Th0rgal/starknet-rs.git", branch = "feat/starknet-id" }
starknet = "0.6.0"
starknet-id = { git = "https://github.com/starknet-id/starknet-id.rs.git", branch = "master" }
axum = "0.6.17"
toml = "0.5.10"
serde = { version = "1.0.152", features = ["derive"] }
Expand All @@ -15,7 +16,7 @@ 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 = "0.11.17"
reqwest = { version = "0.11.17", features = ["json"] }
rand = "0.8.5"
async-trait = "0.1.68"
percent-encoding = "2.3.0"
Expand Down
4 changes: 4 additions & 0 deletions config.template.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ contract = "0xXXXXXXXXXXXX"
contract = "0xXXXXXXXXXXXX"
[quests.myswap]
contract = "0xXXXXXXXXXXXX"
[quests.braavos]
api_key_user = "xxxxxx"
api_key_claimed_mission = "xxxxxx"

[twitter]
oauth2_clientid = "xxxxxx"
Expand All @@ -41,6 +44,7 @@ app_link = "https://starknet.quest"
api_link = "https://api.starknet.quest"
is_testnet = false
proxy = "xxxxx"
rpc_url = "xxxxx"

[starkscan]
api_key = "xxxxxx"
Expand Down
1 change: 1 addition & 0 deletions src/common/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod verify_has_nft;
pub mod verify_has_root_domain;
pub mod verify_has_root_or_braavos_domain;
pub mod verify_quiz;
21 changes: 10 additions & 11 deletions src/common/verify_has_root_domain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use crate::{
use axum::{http::StatusCode, response::IntoResponse, Json};
use serde_json::json;
use starknet::{
core::types::{BlockId, CallFunction, FieldElement},
core::types::{BlockId, BlockTag, FieldElement, FunctionCall},
macros::selector,
providers::Provider,
};
Expand All @@ -23,37 +23,36 @@ pub async fn execute_has_root_domain(
// get starkname from address
let call_result = state
.provider
.call_contract(
CallFunction {
.call(
FunctionCall {
contract_address: state.conf.starknetid_contracts.naming_contract,
entry_point_selector: selector!("address_to_domain"),
calldata: vec![*addr],
},
BlockId::Latest,
BlockId::Tag(BlockTag::Latest),
)
.await;

match call_result {
Ok(result) => {
let domain_len =
i64::from_str_radix(&FieldElement::to_string(&result.result[0]), 16).unwrap();
let domain_len = i64::from_str_radix(&FieldElement::to_string(&result[0]), 16).unwrap();

if domain_len == 1 {
// get expiry
let Ok(expiry_result) = state
.provider
.call_contract(
CallFunction {
.call(
FunctionCall {
contract_address: state.conf.starknetid_contracts.naming_contract,
entry_point_selector: selector!("domain_to_expiry"),
calldata: vec![ FieldElement::ONE, result.result[1] ],
calldata: vec![ FieldElement::ONE, result[1] ],
},
BlockId::Latest,
BlockId::Tag(BlockTag::Latest),
)
.await else {
return get_error("error querying expiry".to_string())
};
let Ok(expiry) : Result<u64, _> = expiry_result.result[0].try_into() else {
let Ok(expiry) : Result<u64, _> = expiry_result[0].try_into() else {
return get_error("error reading expiry".to_string())
};
let now = match SystemTime::now().duration_since(UNIX_EPOCH) {
Expand Down
90 changes: 90 additions & 0 deletions src/common/verify_has_root_or_braavos_domain.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
use std::{
str::FromStr,
sync::Arc,
time::{SystemTime, UNIX_EPOCH},
};

use crate::{
models::AppState,
utils::{get_error, CompletedTasksTrait},
};
use axum::{http::StatusCode, response::IntoResponse, Json};
use serde_json::json;
use starknet::{
core::types::{BlockId, BlockTag, FieldElement, FunctionCall},
macros::selector,
providers::Provider,
};
use starknet_id::decode;

pub async fn verify_has_root_or_braavos_domain(
state: Arc<AppState>,
addr: &FieldElement,
task_id: u32,
) -> impl IntoResponse {
// get starkname from address
let call_result = state
.provider
.call(
FunctionCall {
contract_address: state.conf.starknetid_contracts.naming_contract,
entry_point_selector: selector!("address_to_domain"),
calldata: vec![*addr],
},
BlockId::Tag(BlockTag::Latest),
)
.await;

match call_result {
Ok(result) => {
let domain_len = i64::from_str_radix(&FieldElement::to_string(&result[0]), 16).unwrap();

if domain_len == 1 {
// get expiry
let Ok(expiry_result) = state
.provider
.call(
FunctionCall {
contract_address: state.conf.starknetid_contracts.naming_contract,
entry_point_selector: selector!("domain_to_expiry"),
calldata: vec![ FieldElement::ONE, result[1] ],
},
BlockId::Tag(BlockTag::Latest),
)
.await else {
return get_error("error querying expiry".to_string())
};
let Ok(expiry) : Result<u64, _> = expiry_result[0].try_into() else {
return get_error("error reading expiry".to_string())
};
let now = match SystemTime::now().duration_since(UNIX_EPOCH) {
Ok(n) => n.as_secs(),
Err(_) => return get_error("system time before UNIX EPOCH".to_string()),
};
if expiry < now {
return get_error("expired domain".to_string());
}

match state.upsert_completed_task(*addr, task_id).await {
Ok(_) => (StatusCode::OK, Json(json!({"res": true}))).into_response(),
Err(e) => get_error(format!("{}", e)),
}
} else if domain_len == 2 {
if decode(result[2]) == "braavos" {
match state.upsert_completed_task(*addr, task_id).await {
Ok(_) => (StatusCode::OK, Json(json!({"res": true}))).into_response(),
Err(e) => get_error(format!("{}", e)),
}
} else {
get_error("Invalid subdomain: only Braavos subdomains are eligible".to_string())
}
} else {
get_error(
"Invalid domain: only root domains & Braavos subdomains are eligible"
.to_string(),
)
}
}
Err(e) => get_error(format!("{}", e)),
}
}
9 changes: 8 additions & 1 deletion src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ pub_struct!(Clone, Deserialize; Variables {
app_link: String,
api_link: String,
is_testnet: bool,
proxy: Option<String>
proxy: Option<String>,
rpc_url: String,
});

pub_struct!(Clone, Deserialize; StarknetIdContracts {
Expand All @@ -44,12 +45,18 @@ pub_struct!(Clone, Deserialize; Contract {
contract: FieldElement,
});

pub_struct!(Clone, Deserialize; Braavos {
api_key_user: String,
api_key_claimed_mission: String,
});

pub_struct!(Clone, Deserialize; Quests {
sithswap: Pairs,
zklend: Contract,
jediswap: Pairs,
ekubo: Contract,
myswap: Contract,
braavos: Braavos,
});

pub_struct!(Clone, Deserialize; Twitter {
Expand Down
21 changes: 10 additions & 11 deletions src/endpoints/achievements/verify_has_domain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use axum::{
};
use serde_json::json;
use starknet::{
core::types::{BlockId, CallFunction, FieldElement},
core::types::{BlockId, BlockTag, FieldElement, FunctionCall},
macros::selector,
providers::Provider,
};
Expand All @@ -29,37 +29,36 @@ pub async fn handler(
// get starkname from address
let call_result = state
.provider
.call_contract(
CallFunction {
.call(
FunctionCall {
contract_address: state.conf.starknetid_contracts.naming_contract,
entry_point_selector: selector!("address_to_domain"),
calldata: vec![*addr],
},
BlockId::Latest,
BlockId::Tag(BlockTag::Latest),
)
.await;

match call_result {
Ok(result) => {
let domain_len =
i64::from_str_radix(&FieldElement::to_string(&result.result[0]), 16).unwrap();
let domain_len = i64::from_str_radix(&FieldElement::to_string(&result[0]), 16).unwrap();

if domain_len == 1 {
// get expiry
let Ok(expiry_result) = state
.provider
.call_contract(
CallFunction {
.call(
FunctionCall {
contract_address: state.conf.starknetid_contracts.naming_contract,
entry_point_selector: selector!("domain_to_expiry"),
calldata: vec![ FieldElement::ONE, result.result[1] ],
calldata: vec![ FieldElement::ONE, result[1] ],
},
BlockId::Latest,
BlockId::Tag(BlockTag::Latest),
)
.await else {
return get_error("error querying expiry".to_string())
};
let Ok(expiry) : Result<u64, _> = expiry_result.result[0].try_into() else {
let Ok(expiry) : Result<u64, _> = expiry_result[0].try_into() else {
return get_error("error reading expiry".to_string())
};
let now = match SystemTime::now().duration_since(UNIX_EPOCH) {
Expand Down
1 change: 1 addition & 0 deletions src/endpoints/quests/braavos/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod starknetid;
101 changes: 101 additions & 0 deletions src/endpoints/quests/braavos/starknetid/claimable.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
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 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 = 100;
const TASK_IDS: &[u32] = &[46, 47, 48, 49, 50];
const LAST_TASK: u32 = TASK_IDS[4];
const NFT_LEVEL: u32 = 18;

#[derive(Deserialize)]
pub struct ClaimableQuery {
addr: FieldElement,
}

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()),
}
}
6 changes: 6 additions & 0 deletions src/endpoints/quests/braavos/starknetid/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
pub mod claimable;
pub mod verify_has_domain;
pub mod verify_has_mission;
pub mod verify_twitter_fw_braavos;
pub mod verify_twitter_fw_sid;
pub mod verify_twitter_fw_sq;
Loading

0 comments on commit f27f28e

Please sign in to comment.