From 873a8988c39440b5ec0ef032cad3f38ce92d8595 Mon Sep 17 00:00:00 2001 From: Iris Date: Tue, 24 Oct 2023 12:28:56 +0200 Subject: [PATCH 1/3] feat: add layerswap quest --- .../quests/element/layerswap/claimable.rs | 101 ++++++++++++++++++ src/endpoints/quests/element/layerswap/mod.rs | 4 + .../element/layerswap/verify_has_bridged.rs | 77 +++++++++++++ .../element/layerswap/verify_twitter_fw.rs | 28 +++++ .../element/layerswap/verify_twitter_rt.rs | 28 +++++ src/endpoints/quests/element/mod.rs | 1 + src/endpoints/quests/uri.rs | 11 ++ src/main.rs | 16 +++ 8 files changed, 266 insertions(+) create mode 100644 src/endpoints/quests/element/layerswap/claimable.rs create mode 100644 src/endpoints/quests/element/layerswap/mod.rs create mode 100644 src/endpoints/quests/element/layerswap/verify_has_bridged.rs create mode 100644 src/endpoints/quests/element/layerswap/verify_twitter_fw.rs create mode 100644 src/endpoints/quests/element/layerswap/verify_twitter_rt.rs diff --git a/src/endpoints/quests/element/layerswap/claimable.rs b/src/endpoints/quests/element/layerswap/claimable.rs new file mode 100644 index 00000000..19c78ce2 --- /dev/null +++ b/src/endpoints/quests/element/layerswap/claimable.rs @@ -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 = 18; +const TASK_IDS: &[u32] = &[70, 71, 72]; +const LAST_TASK: u32 = TASK_IDS[2]; +const NFT_LEVEL: u32 = 24; + +#[derive(Deserialize)] +pub struct ClaimableQuery { + addr: FieldElement, +} + +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/element/layerswap/mod.rs b/src/endpoints/quests/element/layerswap/mod.rs new file mode 100644 index 00000000..90105076 --- /dev/null +++ b/src/endpoints/quests/element/layerswap/mod.rs @@ -0,0 +1,4 @@ +pub mod claimable; +pub mod verify_has_bridged; +pub mod verify_twitter_fw; +pub mod verify_twitter_rt; diff --git a/src/endpoints/quests/element/layerswap/verify_has_bridged.rs b/src/endpoints/quests/element/layerswap/verify_has_bridged.rs new file mode 100644 index 00000000..ca4244ec --- /dev/null +++ b/src/endpoints/quests/element/layerswap/verify_has_bridged.rs @@ -0,0 +1,77 @@ +use std::sync::Arc; + +use crate::{ + models::{AppState, VerifyQuery}, + utils::{get_error, to_hex, CompletedTasksTrait}, +}; +use axum::{ + extract::{Query, State}, + http::StatusCode, + response::IntoResponse, + Json, +}; +use serde_json::json; + +#[derive(Debug, serde::Deserialize)] +struct UserTaskStatus { + address: Vec, +} + +#[derive(Debug, serde::Deserialize)] +#[allow(non_snake_case)] +struct Data { + userTaskStatus: UserTaskStatus, +} + +#[derive(Debug, serde::Deserialize)] +struct Response { + data: Data, +} + +pub async fn handler( + State(state): State>, + Query(query): Query, +) -> impl IntoResponse { + let task_id = 70; + let hex_addr = to_hex(query.addr); + + let graphql_url = "https://actapi.orbiter.finance/graphql/activity"; + let graphql_query = r#" + query info($address: String!) { + userTaskStatus(address: $address, taskId: "5", verify: "aIHcjkNqpcD") { + address + } + } + "#; + let variables = serde_json::json!({ "address": hex_addr }); + + let client = reqwest::Client::new(); + let response_result = client + .post(graphql_url) + .json(&serde_json::json!({ + "query": graphql_query, + "variables": variables + })) + .send() + .await; + + match response_result { + Ok(response) => match response.json::().await { + Ok(res) => { + if res.data.userTaskStatus.address.is_empty() { + get_error("You haven't bridge ETH to Starknet using Orbiter.".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!( + "Failed to get JSON response while fetching user info: {}", + e + )), + }, + Err(e) => get_error(format!("Failed to fetch user info: {}", e)), + } +} diff --git a/src/endpoints/quests/element/layerswap/verify_twitter_fw.rs b/src/endpoints/quests/element/layerswap/verify_twitter_fw.rs new file mode 100644 index 00000000..e2aadc9c --- /dev/null +++ b/src/endpoints/quests/element/layerswap/verify_twitter_fw.rs @@ -0,0 +1,28 @@ +use std::sync::Arc; + +use crate::{ + models::{AppState, VerifyQuery}, + utils::{get_error, CompletedTasksTrait}, +}; +use axum::{ + extract::{Query, State}, + http::StatusCode, + response::IntoResponse, + Json, +}; +use serde_json::json; +use starknet::core::types::FieldElement; + +pub async fn handler( + State(state): State>, + Query(query): Query, +) -> impl IntoResponse { + let task_id = 71; + if query.addr == FieldElement::ZERO { + return get_error("Please connect your wallet first".to_string()); + } + 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/element/layerswap/verify_twitter_rt.rs b/src/endpoints/quests/element/layerswap/verify_twitter_rt.rs new file mode 100644 index 00000000..bd0fac98 --- /dev/null +++ b/src/endpoints/quests/element/layerswap/verify_twitter_rt.rs @@ -0,0 +1,28 @@ +use std::sync::Arc; + +use crate::{ + models::{AppState, VerifyQuery}, + utils::{get_error, CompletedTasksTrait}, +}; +use axum::{ + extract::{Query, State}, + http::StatusCode, + response::IntoResponse, + Json, +}; +use serde_json::json; +use starknet::core::types::FieldElement; + +pub async fn handler( + State(state): State>, + Query(query): Query, +) -> impl IntoResponse { + let task_id = 72; + if query.addr == FieldElement::ZERO { + return get_error("Please connect your wallet first".to_string()); + } + 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/element/mod.rs b/src/endpoints/quests/element/mod.rs index bbdf7626..2eed0337 100644 --- a/src/endpoints/quests/element/mod.rs +++ b/src/endpoints/quests/element/mod.rs @@ -1,2 +1,3 @@ pub mod briq; pub mod element; +pub mod layerswap; diff --git a/src/endpoints/quests/uri.rs b/src/endpoints/quests/uri.rs index 8d9be5e6..cf21fe30 100644 --- a/src/endpoints/quests/uri.rs +++ b/src/endpoints/quests/uri.rs @@ -274,6 +274,17 @@ pub async fn handler( ) .into_response(), + Some(24) => ( + StatusCode::OK, + Json(TokenURI { + name: "The Layerswap Element Gemstone".into(), + description: "A Layerswap Element Gemstone NFT can be won for successfully finishing the Quest".into(), + image: format!("{}/element/layerswapGem.webp", state.conf.variables.app_link), + attributes: None, + }), + ) + .into_response(), + _ => get_error("Error, this level is not correct".into()), } } diff --git a/src/main.rs b/src/main.rs index 83402e56..84099e38 100644 --- a/src/main.rs +++ b/src/main.rs @@ -294,6 +294,22 @@ async fn main() { "/quests/element/briq/claimable", get(endpoints::quests::element::briq::claimable::handler), ) + .route( + "/quests/element/layerswap/verify_has_bridged", + get(endpoints::quests::element::layerswap::verify_has_bridged::handler), + ) + .route( + "/quests/element/layerswap/verify_twitter_fw", + get(endpoints::quests::element::layerswap::verify_twitter_fw::handler), + ) + .route( + "/quests/element/layerswap/verify_twitter_rt", + get(endpoints::quests::element::layerswap::verify_twitter_rt::handler), + ) + .route( + "/quests/element/layerswap/claimable", + get(endpoints::quests::element::layerswap::claimable::handler), + ) .route( "/achievements/verify_default", get(endpoints::achievements::verify_default::handler), From 2a77dbed828b73dce19b8ac0c7f51382d947c3eb Mon Sep 17 00:00:00 2001 From: Iris Date: Tue, 24 Oct 2023 17:46:52 +0200 Subject: [PATCH 2/3] feat: update verify_has_bridge endpoint --- .../element/layerswap/verify_has_bridged.rs | 73 +++++++++---------- 1 file changed, 36 insertions(+), 37 deletions(-) diff --git a/src/endpoints/quests/element/layerswap/verify_has_bridged.rs b/src/endpoints/quests/element/layerswap/verify_has_bridged.rs index ca4244ec..c7eee09e 100644 --- a/src/endpoints/quests/element/layerswap/verify_has_bridged.rs +++ b/src/endpoints/quests/element/layerswap/verify_has_bridged.rs @@ -10,22 +10,23 @@ use axum::{ response::IntoResponse, Json, }; +use serde::Deserialize; use serde_json::json; -#[derive(Debug, serde::Deserialize)] -struct UserTaskStatus { - address: Vec, +#[derive(Debug, Deserialize)] +struct LayerswapResponse { + data: Option>, + error: Option, } -#[derive(Debug, serde::Deserialize)] -#[allow(non_snake_case)] -struct Data { - userTaskStatus: UserTaskStatus, +#[derive(Debug, Deserialize)] +struct DataEntry { + status: String, } -#[derive(Debug, serde::Deserialize)] -struct Response { - data: Data, +#[derive(Debug, Deserialize)] +struct LayerswapError { + message: String, } pub async fn handler( @@ -33,45 +34,43 @@ pub async fn handler( Query(query): Query, ) -> impl IntoResponse { let task_id = 70; - let hex_addr = to_hex(query.addr); - - let graphql_url = "https://actapi.orbiter.finance/graphql/activity"; - let graphql_query = r#" - query info($address: String!) { - userTaskStatus(address: $address, taskId: "5", verify: "aIHcjkNqpcD") { - address - } - } - "#; - let variables = serde_json::json!({ "address": hex_addr }); - + let url = format!( + "https://bridge-api.layerswap.io/api/explorer/{}", + to_hex(query.addr) + ); let client = reqwest::Client::new(); - let response_result = client - .post(graphql_url) - .json(&serde_json::json!({ - "query": graphql_query, - "variables": variables - })) - .send() - .await; - + let response_result = client.get(url).send().await; match response_result { - Ok(response) => match response.json::().await { + Ok(response) => match response.json::().await { Ok(res) => { - if res.data.userTaskStatus.address.is_empty() { - get_error("You haven't bridge ETH to Starknet using Orbiter.".to_string()) - } else { + if let Some(err) = &res.error { + return get_error(format!("Received error from Layerswap: {}", err.message)); + } + + // Check if there is data and if any entry has "completed" status + if res + .data + .as_ref() + .unwrap_or(&vec![]) + .iter() + .any(|entry| entry.status == "completed") + { 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)), } + } else { + get_error( + "You haven't bridge any ETH or USDC to Starknet using Layerswap." + .to_string(), + ) } } Err(e) => get_error(format!( - "Failed to get JSON response while fetching user info: {}", + "Failed to get JSON response while fetching Layerswap data: {}", e )), }, - Err(e) => get_error(format!("Failed to fetch user info: {}", e)), + Err(e) => get_error(format!("Failed to fetch Layerswap api: {}", e)), } } From b9551ab4568324fe22d12af0d9a2a38357cd8b68 Mon Sep 17 00:00:00 2001 From: Iris Date: Tue, 24 Oct 2023 17:47:59 +0200 Subject: [PATCH 3/3] fix: nft descriptions --- 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 cf21fe30..016603c2 100644 --- a/src/endpoints/quests/uri.rs +++ b/src/endpoints/quests/uri.rs @@ -256,7 +256,7 @@ pub async fn handler( StatusCode::OK, Json(TokenURI { name: "The Element Gemstone".into(), - description: "An Element Gemstone NFT can be won for successfully finishing the Quest".into(), + description: "An Element Gemstone NFT won for successfully finishing the Quest".into(), image: format!("{}/element/elementGem.webp", state.conf.variables.app_link), attributes: None, }), @@ -267,7 +267,7 @@ pub async fn handler( StatusCode::OK, Json(TokenURI { name: "The Briq Element Gemstone".into(), - description: "A Briq Element Gemstone NFT can be won for successfully finishing the Quest".into(), + description: "A Briq Element Gemstone NFT won for successfully finishing the Quest".into(), image: format!("{}/element/briqGem.webp", state.conf.variables.app_link), attributes: None, }), @@ -278,7 +278,7 @@ pub async fn handler( StatusCode::OK, Json(TokenURI { name: "The Layerswap Element Gemstone".into(), - description: "A Layerswap Element Gemstone NFT can be won for successfully finishing the Quest".into(), + description: "A Layerswap Element Gemstone NFT won for successfully finishing the Quest".into(), image: format!("{}/element/layerswapGem.webp", state.conf.variables.app_link), attributes: None, }),