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: get_altcoin_quote endpoint #58

Merged
merged 7 commits into from
Mar 13, 2024
Merged
Show file tree
Hide file tree
Changes from 5 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
14 changes: 14 additions & 0 deletions config.template.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,17 @@ api_key = "xxxxxx"
[solana]
rpc_url = "https://xxxxxxx.solana-mainnet.quiknode.pro/xxxxxxx"
private_key = "xxxxxxx"

[token_support]
whitelisted_tokens = [
# ETH
"0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7",
# STRK
"0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d",
# USDC
"0x053c91253bc9682c04929ca02ed00b3e423f6710d2ee7e0d5ebb06f3ecf368a8",
# USDT
"0x068f5c6a61780768455de69077e07e89787839bf8166decfbf92b645209c0fb8",
]
max_quote_validity = 1000 # in seconds
private_key = "123"
10 changes: 10 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,13 @@ pub_struct!(Clone, Deserialize; Solana {
private_key: FieldElement,
});

pub_struct!(Clone, Deserialize; TokenSupport {
avnu_api: String,
whitelisted_tokens: Vec<FieldElement>,
max_quote_validity: i64,
private_key: FieldElement
});

#[derive(Deserialize)]
struct RawConfig {
server: Server,
Expand All @@ -52,6 +59,7 @@ struct RawConfig {
starkscan: Starkscan,
custom_resolvers: HashMap<String, Vec<String>>,
solana: Solana,
token_support: TokenSupport,
}

pub_struct!(Clone, Deserialize; Config {
Expand All @@ -62,6 +70,7 @@ pub_struct!(Clone, Deserialize; Config {
custom_resolvers: HashMap<String, Vec<String>>,
reversed_resolvers: HashMap<String, String>,
solana: Solana,
token_support: TokenSupport,
});

impl From<RawConfig> for Config {
Expand All @@ -80,6 +89,7 @@ impl From<RawConfig> for Config {
custom_resolvers: raw.custom_resolvers,
reversed_resolvers,
solana: raw.solana,
token_support: raw.token_support,
}
}
}
Expand Down
124 changes: 124 additions & 0 deletions src/endpoints/avnu/get_altcoin_quote.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
use std::sync::Arc;

use axum::{
extract::{Query, State},
http::StatusCode,
response::IntoResponse,
Json,
};
use axum_auto_routes::route;
use chrono::Duration;
use serde::Deserialize;
use serde_json::json;
use starknet::core::{
crypto::{ecdsa_sign, pedersen_hash},
types::FieldElement,
};

use crate::{models::AppState, utils::get_error};

#[derive(Deserialize)]
pub struct AddrQuery {
erc20_addr: FieldElement,
}

#[derive(Deserialize, Debug)]
pub struct AvnuApiResult {
address: FieldElement,
currentPrice: f64,
}

lazy_static::lazy_static! {
static ref QUOTE_STR: FieldElement = FieldElement::from_dec_str("724720344857006587549020016926517802128122613457935427138661").unwrap();
}

#[route(get, "/get_altcoin_quote", crate::endpoints::avnu::get_altcoin_quote)]
pub async fn handler(
State(state): State<Arc<AppState>>,
Query(query): Query<AddrQuery>,
) -> impl IntoResponse {
// check if erc20_addr is whitelisted
if !state
.conf
.token_support
.whitelisted_tokens
.contains(&query.erc20_addr)
{
return get_error("Token not supported".to_string());
}

// fetch quote from avnu api
let url = format!(
"{}/tokens/short?in=0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7",
state.conf.token_support.avnu_api
);
let client = reqwest::Client::new();
match client.get(&url).send().await {
Ok(response) => match response.text().await {
Ok(text) => match serde_json::from_str::<Vec<AvnuApiResult>>(&text) {
Ok(res) => {
let result = res
.iter()
.find(|&api_response| api_response.address == query.erc20_addr);
match result {
Some(data) => {
// compute message hash
let now = chrono::Utc::now();
let max_quote_validity_timestamp = (now
+ Duration::seconds(state.conf.token_support.max_quote_validity))
.timestamp();
let quote = 1.0 / data.currentPrice;
// convert current price to wei and return an integer as AVNU api can use more than 18 decimals
let current_price_wei = (quote * (10u128.pow(18) as f64)).to_string();
let message_hash = pedersen_hash(
Th0rgal marked this conversation as resolved.
Show resolved Hide resolved
&pedersen_hash(
&pedersen_hash(
&query.erc20_addr,
&FieldElement::from_dec_str(
current_price_wei.to_string().as_str(),
)
.unwrap(),
),
&FieldElement::from_dec_str(
max_quote_validity_timestamp.to_string().as_str(),
)
.unwrap(),
),
&QUOTE_STR,
);
match ecdsa_sign(
&state.conf.token_support.private_key.clone(),
&message_hash,
) {
Ok(signature) => (
StatusCode::OK,
Json(json!({
"quote": current_price_wei,
"r": signature.r,
"s": signature.s,
"max_quote_validity": max_quote_validity_timestamp
})),
)
.into_response(),
Err(e) => get_error(format!(
"Error while generating Starknet signature: {}",
e
)),
}
}
None => get_error("Token address not found".to_string()),
}
}
Err(e) => get_error(format!(
"Failed to deserialize result from AVNU API: {} for response: {}",
e, text
)),
},
Err(e) => get_error(format!(
"Failed to get JSON response while fetching token quote: {}",
e
)),
},
Err(e) => get_error(format!("Failed to fetch quote from AVNU api: {}", e)),
}
}
1 change: 1 addition & 0 deletions src/endpoints/avnu/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod get_altcoin_quote;
1 change: 1 addition & 0 deletions src/endpoints/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ pub mod addr_to_external_domains;
pub mod addr_to_full_ids;
pub mod addr_to_token_id;
pub mod addrs_to_domains;
pub mod avnu;
pub mod crosschain;
pub mod data_to_ids;
pub mod domain_to_addr;
Expand Down
103 changes: 62 additions & 41 deletions src/endpoints/stats/count_club_domains.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ use axum_auto_routes::route;
use futures::TryStreamExt;
use mongodb::bson::{self, doc, Bson};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use std::collections::HashMap;
use std::sync::Arc;

#[derive(Serialize)]
pub struct CountClubDomainsData {
Expand All @@ -23,15 +23,21 @@ pub struct CountClubDomainsQuery {
since: i64,
}

#[route(get, "/stats/count_club_domains", crate::endpoints::stats::count_club_domains)]
#[route(
get,
"/stats/count_club_domains",
crate::endpoints::stats::count_club_domains
)]
pub async fn handler(
State(state): State<Arc<AppState>>,
Query(query): Query<CountClubDomainsQuery>,
) -> impl IntoResponse {
let mut headers = HeaderMap::new();
headers.insert("Cache-Control", HeaderValue::from_static("max-age=60"));

let domain_collection = state.starknetid_db.collection::<mongodb::bson::Document>("domains");
let domain_collection = state
.starknetid_db
.collection::<mongodb::bson::Document>("domains");
let subdomain_collection = state
.starknetid_db
.collection::<mongodb::bson::Document>("custom_resolutions");
Expand Down Expand Up @@ -162,51 +168,66 @@ pub async fn handler(
}
], None).await.unwrap().try_collect::<Vec<bson::Document>>().await.unwrap();

let mut count_99 = 0;
let mut count_999 = 0;
let mut count_10k = 0;

let mut output: Vec<HashMap<String, i32>> = Vec::new();
let mut output_map: HashMap<String, i32> = HashMap::new();
let mut count_99 = 0;
let mut count_999 = 0;
let mut count_10k = 0;

for doc in &db_output {
if let Ok(club) = doc.get_str("club") {
match club {
"99" => count_99 = doc.get_i32("count").unwrap_or_default(),
"999" => count_999 = doc.get_i32("count").unwrap_or_default(),
"10k" => count_10k = doc.get_i32("count").unwrap_or_default(),
_ => (),
}
let mut output: Vec<HashMap<String, i32>> = Vec::new();
let mut output_map: HashMap<String, i32> = HashMap::new();

for doc in &db_output {
if let Ok(club) = doc.get_str("club") {
match club {
"99" => count_99 = doc.get_i32("count").unwrap_or_default(),
"999" => count_999 = doc.get_i32("count").unwrap_or_default(),
"10k" => count_10k = doc.get_i32("count").unwrap_or_default(),
_ => (),
}
}
}

for doc in db_output {
if let Ok(club) = doc.get_str("club") {
match club {
"two_letters" => {
output_map.insert(club.to_string(), doc.get_i32("count").unwrap_or_default() + count_99);
}
"three_letters" => {
output_map.insert(club.to_string(), doc.get_i32("count").unwrap_or_default() + count_999);
}
"four_letters" => {
output_map.insert(club.to_string(), doc.get_i32("count").unwrap_or_default() + count_10k);
}
_ => {
output_map.insert(club.to_string(), doc.get_i32("count").unwrap_or_default());
}
for doc in db_output {
if let Ok(club) = doc.get_str("club") {
match club {
"two_letters" => {
output_map.insert(
club.to_string(),
doc.get_i32("count").unwrap_or_default() + count_99,
);
}
"three_letters" => {
output_map.insert(
club.to_string(),
doc.get_i32("count").unwrap_or_default() + count_999,
);
}
"four_letters" => {
output_map.insert(
club.to_string(),
doc.get_i32("count").unwrap_or_default() + count_10k,
);
}
_ => {
output_map.insert(club.to_string(), doc.get_i32("count").unwrap_or_default());
}
}
output.push(output_map.clone());
output_map.clear();
}
output.push(output_map.clone());
output_map.clear();
}

for doc in subdomain_output {
output_map.insert(doc.get_str("club").unwrap_or_default().to_string(), doc.get_i32("count").unwrap_or_default());
output_map.insert(doc.get_str("club").unwrap_or_default().to_string(), doc.get_i32("count").unwrap_or_default());
output.push(output_map.clone());
output_map.clear();
}
for doc in subdomain_output {
output_map.insert(
doc.get_str("club").unwrap_or_default().to_string(),
doc.get_i32("count").unwrap_or_default(),
);
output_map.insert(
doc.get_str("club").unwrap_or_default().to_string(),
doc.get_i32("count").unwrap_or_default(),
);
output.push(output_map.clone());
output_map.clear();
}

(StatusCode::OK, headers, Json(output)).into_response()
(StatusCode::OK, headers, Json(output)).into_response()
}