From 37309da3f238cc6c089ba91d70a122b10283a3f6 Mon Sep 17 00:00:00 2001 From: Iris Date: Tue, 22 Aug 2023 11:13:32 +0200 Subject: [PATCH 1/2] feat: update verify_default endpoint --- config.template.toml | 12 +++- src/common/mod.rs | 3 +- src/common/verify_has_nft.rs | 61 ++++++++++++++++++++ src/config.rs | 19 ++++++ src/endpoints/achievements/fetch.rs | 4 +- src/endpoints/achievements/verify_default.rs | 41 +++++++++---- src/models.rs | 33 +++++++++++ 7 files changed, 159 insertions(+), 14 deletions(-) create mode 100644 src/common/verify_has_nft.rs diff --git a/config.template.toml b/config.template.toml index f622e519..d3794b7b 100644 --- a/config.template.toml +++ b/config.template.toml @@ -72,4 +72,14 @@ layout = "illustrated_left" question = "Which number is represented on the image?" options = ["1", "2"] correct_answers = [0] -image_for_layout = "/example/one.webp" \ No newline at end of file +image_for_layout = "/example/one.webp" + +[starkscan] +api_key = "xxxxxx" + + +[achievements] +[achievements.braavos] +contract = "0x00057c4b510d66eb1188a7173f31cccee47b9736d40185da8144377b896d5ff3" +[achievements.argent] +contract = "0x01b22f7a9d18754c994ae0ee9adb4628d414232e3ebd748c386ac286f86c3066" \ No newline at end of file diff --git a/src/common/mod.rs b/src/common/mod.rs index 268565d3..0d33149b 100644 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -1,2 +1,3 @@ +pub mod verify_has_nft; pub mod verify_has_root_domain; -pub mod verify_quiz; \ No newline at end of file +pub mod verify_quiz; diff --git a/src/common/verify_has_nft.rs b/src/common/verify_has_nft.rs new file mode 100644 index 00000000..6bc5912f --- /dev/null +++ b/src/common/verify_has_nft.rs @@ -0,0 +1,61 @@ +use crate::{config::Config, models::StarkscanQuery, utils::to_hex}; +use starknet::core::types::FieldElement; + +pub async fn execute_has_nft( + config: &Config, + addr: FieldElement, + contract: FieldElement, + limit: u32, +) -> bool { + let url = format!( + "https://api.starkscan.co/api/v0/nfts?contract_address={}&owner_address={}", + to_hex(contract), + to_hex(addr) + ); + let client = reqwest::Client::new(); + match client + .get(&url) + .header("accept", "application/json") + .header("x-api-key", config.starkscan.api_key.clone()) + .send() + .await + { + Ok(response) => { + match response.text().await { + Ok(text) => { + match serde_json::from_str::(&text) { + Ok(res) => { + // Remove duplicates + let nft_data = res.data; + let mut unique_nfts: Vec = Vec::new(); + for nft in nft_data { + if nft.name.is_some() { + let name = nft.name.unwrap(); + if !unique_nfts.contains(&name) { + unique_nfts.push(name); + } + } + } + unique_nfts.len() >= limit as usize + } + Err(e) => { + println!("Failed to deserialize result from Starkscan API: {}", e); + false + } + } + } + Err(e) => { + println!( + "Failed to get JSON response while fetching user NFT data: {}", + e + ); + false + } + } + } + Err(e) => { + println!("Failed to fetch user NFTs from API: {}", e); + false + } + } +} diff --git a/src/config.rs b/src/config.rs index 829270e6..8987aba0 100644 --- a/src/config.rs +++ b/src/config.rs @@ -92,6 +92,23 @@ pub_struct!(Clone, Deserialize; Quiz { questions: Vec, }); +pub_struct!(Clone, Deserialize; Starkscan { + api_key: String, +}); + +pub_struct!(Clone, Deserialize; Braavos { + contract: FieldElement, +}); + +pub_struct!(Clone, Deserialize; Argent { + contract: FieldElement, +}); + +pub_struct!(Clone, Deserialize; Achievements { + braavos: Braavos, + argent: Argent, +}); + pub_struct!(Clone, Deserialize; Config { server: Server, database: Database, @@ -102,6 +119,8 @@ pub_struct!(Clone, Deserialize; Config { twitter: Twitter, discord: Discord, quizzes: HashMap, + starkscan: Starkscan, + achievements: Achievements, }); pub fn load() -> Config { diff --git a/src/endpoints/achievements/fetch.rs b/src/endpoints/achievements/fetch.rs index 31c7b1f6..65e77ef3 100644 --- a/src/endpoints/achievements/fetch.rs +++ b/src/endpoints/achievements/fetch.rs @@ -54,6 +54,7 @@ pub async fn handler( "_id": 0, "category_name": "$name", "category_desc": "$desc", + "category_img_url": "$img_url", "achievements": { "name": "$achievement.name", "short_desc": "$achievement.short_desc", @@ -78,7 +79,7 @@ pub async fn handler( }, doc! { "$group": { - "_id": { "category_name": "$category_name", "category_desc": "$category_desc" }, + "_id": { "category_name": "$category_name", "category_desc": "$category_desc", "category_img_url": "$category_img_url" }, "achievements": { "$push": "$achievements" } } }, @@ -86,6 +87,7 @@ pub async fn handler( "$project": { "category_name": "$_id.category_name", "category_desc": "$_id.category_desc", + "category_img_url": "$_id.category_img_url", "achievements": 1, "_id": 0 } diff --git a/src/endpoints/achievements/verify_default.rs b/src/endpoints/achievements/verify_default.rs index cd62c6d8..b3c9fcf2 100644 --- a/src/endpoints/achievements/verify_default.rs +++ b/src/endpoints/achievements/verify_default.rs @@ -1,6 +1,8 @@ use std::sync::Arc; use crate::{ + common::verify_has_nft::execute_has_nft, + config::Config, models::{AchievedDocument, AppState, VerifyAchievementQuery}, utils::{get_error, AchievementsTrait}, }; @@ -14,6 +16,20 @@ use mongodb::bson::doc; use serde_json::json; use starknet::core::types::FieldElement; +fn get_args(config: Config, achievement_id: u32) -> Result<(FieldElement, u32), String> { + match achievement_id { + // ArgentX Xplorer NFTs + 1 => Ok((config.achievements.argent.contract, 1)), + 2 => Ok((config.achievements.argent.contract, 4)), + 3 => Ok((config.achievements.argent.contract, 8)), + // Braavos Journey NFTs + 4 => Ok((config.achievements.braavos.contract, 1)), + 5 => Ok((config.achievements.braavos.contract, 3)), + 6 => Ok((config.achievements.braavos.contract, 6)), + _ => Err("Invalid achievement ID".to_string()), + } +} + pub async fn handler( State(state): State>, Query(query): Query, @@ -30,19 +46,22 @@ pub async fn handler( }; match achieved_collection.find_one(filter, None).await { Ok(Some(_)) => (StatusCode::OK, Json(json!({"achieved": true}))).into_response(), - Ok(None) => match state.get_achievement(achievement_id).await { - Ok(Some(achievement)) => { - // todo: add verifying logic here - match state - .upsert_completed_achievement(addr, achievement_id) - .await - { - Ok(_) => (StatusCode::OK, Json(json!({"achieved": true}))).into_response(), - Err(e) => get_error(format!("{}", e)), + Ok(None) => match get_args(state.conf.clone(), achievement_id) { + Ok((contract, limit)) => { + let is_achieved = execute_has_nft(&state.conf, addr, contract, limit).await; + if is_achieved { + match state + .upsert_completed_achievement(addr, achievement_id) + .await + { + Ok(_) => (StatusCode::OK, Json(json!({"achieved": true}))).into_response(), + Err(e) => get_error(format!("{}", e)), + } + } else { + (StatusCode::OK, Json(json!({"achieved": false}))).into_response() } } - Ok(None) => get_error("Achievement not found".to_string()), - Err(e) => get_error(format!("Error querying achievement : {}", e)), + Err(e) => get_error(e), }, Err(e) => get_error(format!("Error querying user achievement : {}", e)), } diff --git a/src/models.rs b/src/models.rs index 478f5532..f33c79b8 100644 --- a/src/models.rs +++ b/src/models.rs @@ -1,5 +1,6 @@ use mongodb::{bson, Database}; use serde::{Deserialize, Serialize}; +use serde_json::Value; use starknet::{core::types::FieldElement, providers::SequencerGatewayProvider}; use crate::config::Config; @@ -98,11 +99,13 @@ pub_struct!(Debug, Serialize, Deserialize; AchievementCategoryDocument { id: u32, name: String, desc: String, + img_url: String, }); pub_struct!(Debug, Serialize, Deserialize; UserAchievements { category_name: String, category_desc: String, + category_img_url: String, achievements: Vec, }); @@ -114,3 +117,33 @@ pub_struct!(Debug, Serialize, Deserialize; UserAchievement { completed: bool, verify_type: String, }); + +pub_struct!(Debug, Serialize, Deserialize; NftBalance { + contract_address: String, + token_id: String, + owner_address: String, + balance: String, +}); + +pub_struct!(Debug, Serialize, Deserialize; Nft { + nft_id: String, + contract_address: String, + token_id: String, + name: Option, + description: Option, + external_url: Option, + attributes: Option, + image_url: Option, + image_small_url: Option, + image_medium_url: Option, + animation_url: Option, + minted_by_address: String, + minted_at_transaction_hash: String, + minted_at_timestamp: i64, + balance: Option, +}); + +pub_struct!(Debug, Serialize, Deserialize; StarkscanQuery { + next_url: Option, + data: Vec, +}); From 4b8fd667ba7f3e1adcb3abeedebf5b9e663c8546 Mon Sep 17 00:00:00 2001 From: Iris Date: Tue, 22 Aug 2023 11:45:44 +0200 Subject: [PATCH 2/2] fix: add id in achievements array --- src/endpoints/achievements/fetch.rs | 1 + src/models.rs | 1 + 2 files changed, 2 insertions(+) diff --git a/src/endpoints/achievements/fetch.rs b/src/endpoints/achievements/fetch.rs index 65e77ef3..19568bd0 100644 --- a/src/endpoints/achievements/fetch.rs +++ b/src/endpoints/achievements/fetch.rs @@ -56,6 +56,7 @@ pub async fn handler( "category_desc": "$desc", "category_img_url": "$img_url", "achievements": { + "id": "$achievement.id", "name": "$achievement.name", "short_desc": "$achievement.short_desc", "title": { diff --git a/src/models.rs b/src/models.rs index f33c79b8..c425f39f 100644 --- a/src/models.rs +++ b/src/models.rs @@ -110,6 +110,7 @@ pub_struct!(Debug, Serialize, Deserialize; UserAchievements { }); pub_struct!(Debug, Serialize, Deserialize; UserAchievement { + id: u32, name: String, short_desc: String, title: String,