Skip to content

Commit

Permalink
Merge pull request #58 from starknet-id/feat/get_altcoin_quote_cairo2
Browse files Browse the repository at this point in the history
Feat: get_altcoin_quote endpoint
  • Loading branch information
Th0rgal authored Mar 13, 2024
2 parents d3ec41e + 45c2318 commit f4a4a16
Show file tree
Hide file tree
Showing 5 changed files with 285 additions and 42 deletions.
33 changes: 33 additions & 0 deletions config.template.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,36 @@ api_key = "xxxxxx"
[solana]
rpc_url = "https://xxxxxxx.solana-mainnet.quiknode.pro/xxxxxxx"
private_key = "xxxxxxx"

[altcoins]
avnu_api = "https://starknet.impulse.avnu.fi/v1"
private_key = "123"

# Ethereum (ETH) is not enabled for the moment as it is already supported by default buy.
# [altcoins.ETH]
# address = "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7"
# min_price = 1
# max_price = 1
# decimals = 18
# max_quote_validity = 3600 # in seconds for ETH

[altcoins.STRK]
address = "0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d"
min_price = 500
max_price = 5000
decimals = 18
max_quote_validity = 300 # it moves faster so we reduce the quote validity

[altcoins.USDC]
address = "0x053c91253bc9682c04929ca02ed00b3e423f6710d2ee7e0d5ebb06f3ecf368a8"
min_price = 2000
max_price = 10000
decimals = 6 # not sure really
max_quote_validity = 600

[altcoins.USDT]
address = "0x068f5c6a61780768455de69077e07e89787839bf8166decfbf92b645209c0fb8"
min_price = 2000
max_price = 10000
decimals = 18
max_quote_validity = 600
62 changes: 61 additions & 1 deletion src/config.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use serde::Deserialize;
use serde::{Deserialize, Deserializer};
use starknet::core::types::FieldElement;
use std::collections::HashMap;
use std::env;
Expand Down Expand Up @@ -44,6 +44,28 @@ pub_struct!(Clone, Deserialize; Solana {
private_key: FieldElement,
});

pub_struct!(Clone, Debug, Deserialize; AltcoinData {
address: FieldElement,
min_price: u64,
max_price: u64,
decimals: u32,
max_quote_validity: i64
});

#[derive(Debug, Deserialize)]
struct TempAltcoins {
avnu_api: String,
private_key: FieldElement,
#[serde(flatten)]
data: HashMap<String, AltcoinData>,
}

pub_struct!(Clone, Debug; Altcoins {
avnu_api: String,
private_key: FieldElement,
data: HashMap<FieldElement, AltcoinData>,
});

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

pub_struct!(Clone, Deserialize; Config {
Expand All @@ -62,8 +85,44 @@ pub_struct!(Clone, Deserialize; Config {
custom_resolvers: HashMap<String, Vec<String>>,
reversed_resolvers: HashMap<String, String>,
solana: Solana,
altcoins: Altcoins,
});

impl Altcoins {
fn new(temp: TempAltcoins) -> Self {
let data: HashMap<FieldElement, AltcoinData> = temp
.data
.into_values()
.map(|val| {
let altcoin_data = AltcoinData {
address: val.address,
min_price: val.min_price,
max_price: val.max_price,
decimals: val.decimals,
max_quote_validity: val.max_quote_validity,
};
(val.address, altcoin_data)
})
.collect();

Altcoins {
avnu_api: temp.avnu_api,
private_key: temp.private_key,
data,
}
}
}

impl<'de> Deserialize<'de> for Altcoins {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let temp = TempAltcoins::deserialize(deserializer)?;
Ok(Altcoins::new(temp))
}
}

impl From<RawConfig> for Config {
fn from(raw: RawConfig) -> Self {
let mut reversed_resolvers = HashMap::new();
Expand All @@ -80,6 +139,7 @@ impl From<RawConfig> for Config {
custom_resolvers: raw.custom_resolvers,
reversed_resolvers,
solana: raw.solana,
altcoins: raw.altcoins,
}
}
}
Expand Down
128 changes: 128 additions & 0 deletions src/endpoints/get_altcoin_quote.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
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::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.altcoins.data.contains_key(&query.erc20_addr) {
return get_error("Token not supported".to_string());
}

let altcoin_data = state.conf.altcoins.data.get(&query.erc20_addr).unwrap();
// fetch quote from avnu api
let url = format!(
"{}/tokens/short?in=0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7",
state.conf.altcoins.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_validity_timestamp = (now
+ Duration::seconds(altcoin_data.max_quote_validity))
.timestamp();
let quote = 1.0 / data.currentPrice;
// check if quote is within the valid range
if quote < altcoin_data.min_price as f64
|| quote > altcoin_data.max_price as f64
{
return get_error("Quote out of range".to_string());
}
// 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(altcoin_data.decimals) as f64)) as u128)
.to_string();
let message_hash = pedersen_hash(
&pedersen_hash(
&pedersen_hash(
&query.erc20_addr,
&FieldElement::from_dec_str(
current_price_wei.to_string().as_str(),
)
.unwrap(),
),
&FieldElement::from_dec_str(
max_validity_timestamp.to_string().as_str(),
)
.unwrap(),
),
&QUOTE_STR,
);
match ecdsa_sign(
&state.conf.altcoins.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_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/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 get_altcoin_quote;
pub mod crosschain;
pub mod data_to_ids;
pub mod domain_to_addr;
Expand Down
Loading

0 comments on commit f4a4a16

Please sign in to comment.