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 all 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
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
Th0rgal marked this conversation as resolved.
Show resolved Hide resolved
|| 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