use std::fmt::Display;
use alloy_primitives::{Address, U256};
use bon::Builder;
use serde::{Deserialize, Serialize};
use url::Url;
use crate::{error_code::TraceId, OdosError, Result};
#[cfg(feature = "v2")]
use {
crate::OdosRouterV2::{inputTokenInfo, outputTokenInfo, swapTokenInfo},
crate::OdosV2Router::{swapCall, OdosV2RouterCalls},
alloy_primitives::Bytes,
tracing::debug,
};
#[cfg(feature = "v3")]
use {
crate::IOdosRouterV3::swapTokenInfo as v3SwapTokenInfo, crate::OdosV3Router::OdosV3RouterCalls,
};
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum ApiHost {
Public,
Enterprise,
}
impl ApiHost {
pub fn base_url(&self) -> Url {
match self {
ApiHost::Public => Url::parse("https://api.odos.xyz/").unwrap(),
ApiHost::Enterprise => Url::parse("https://enterprise-api.odos.xyz/").unwrap(),
}
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum ApiVersion {
V2,
V3,
}
impl ApiVersion {
fn path(&self) -> &'static str {
match self {
ApiVersion::V2 => "v2",
ApiVersion::V3 => "v3",
}
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Deserialize, Serialize)]
pub struct Endpoint {
host: ApiHost,
version: ApiVersion,
}
impl Endpoint {
pub const fn new(host: ApiHost, version: ApiVersion) -> Self {
Self { host, version }
}
pub const fn public_v2() -> Self {
Self::new(ApiHost::Public, ApiVersion::V2)
}
pub const fn public_v3() -> Self {
Self::new(ApiHost::Public, ApiVersion::V3)
}
pub const fn enterprise_v2() -> Self {
Self::new(ApiHost::Enterprise, ApiVersion::V2)
}
pub const fn enterprise_v3() -> Self {
Self::new(ApiHost::Enterprise, ApiVersion::V3)
}
pub fn quote_url(&self) -> Url {
self.host
.base_url()
.join(&format!("sor/quote/{}", self.version.path()))
.unwrap()
}
pub fn assemble_url(&self) -> Url {
self.host.base_url().join("sor/assemble").unwrap()
}
pub const fn host(&self) -> ApiHost {
self.host
}
pub const fn version(&self) -> ApiVersion {
self.version
}
}
impl Default for Endpoint {
fn default() -> Self {
Self::public_v2()
}
}
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct InputToken {
token_address: Address,
amount: String,
}
impl InputToken {
pub fn new(token_address: Address, amount: U256) -> Self {
Self {
token_address,
amount: amount.to_string(),
}
}
}
impl From<(Address, U256)> for InputToken {
fn from((token_address, amount): (Address, U256)) -> Self {
Self::new(token_address, amount)
}
}
impl Display for InputToken {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"InputToken {{ token_address: {}, amount: {} }}",
self.token_address, self.amount
)
}
}
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct OutputToken {
token_address: Address,
proportion: u32,
}
impl OutputToken {
pub fn new(token_address: Address, proportion: u32) -> Self {
Self {
token_address,
proportion,
}
}
}
impl From<(Address, u32)> for OutputToken {
fn from((token_address, proportion): (Address, u32)) -> Self {
Self::new(token_address, proportion)
}
}
impl Display for OutputToken {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"OutputToken {{ token_address: {}, proportion: {} }}",
self.token_address, self.proportion
)
}
}
#[derive(Builder, Clone, Debug, Default, PartialEq, PartialOrd, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct QuoteRequest {
chain_id: u64,
input_tokens: Vec<InputToken>,
output_tokens: Vec<OutputToken>,
slippage_limit_percent: f64,
user_addr: Address,
compact: bool,
simple: bool,
referral_code: u32,
disable_rfqs: bool,
#[builder(default)]
source_blacklist: Vec<String>,
}
#[derive(Clone, Debug, PartialEq, PartialOrd, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SingleQuoteResponse {
block_number: u64,
data_gas_estimate: u64,
gas_estimate: f64,
gas_estimate_value: f64,
gwei_per_gas: f64,
in_amounts: Vec<String>,
in_tokens: Vec<Address>,
in_values: Vec<f64>,
net_out_value: f64,
out_amounts: Vec<String>,
out_tokens: Vec<Address>,
out_values: Vec<f64>,
#[serde(default)]
partner_fee_percent: f64,
path_id: String,
path_viz: Option<String>,
percent_diff: f64,
price_impact: f64,
}
impl SingleQuoteResponse {
pub fn in_amount(&self) -> Option<&String> {
self.in_amounts.first()
}
pub fn data_gas_estimate(&self) -> u64 {
self.data_gas_estimate
}
pub fn get_block_number(&self) -> u64 {
self.block_number
}
pub fn gas_estimate(&self) -> f64 {
self.gas_estimate
}
pub fn gas_estimate_value(&self) -> f64 {
self.gas_estimate_value
}
pub fn gwei_per_gas(&self) -> f64 {
self.gwei_per_gas
}
pub fn in_amounts_iter(&self) -> impl Iterator<Item = &String> {
self.in_amounts.iter()
}
pub fn in_amount_u256(&self) -> Result<U256> {
let amount_str = self
.in_amounts_iter()
.next()
.ok_or_else(|| OdosError::missing_data("Missing input amount"))?;
let amount: u128 = amount_str
.parse()
.map_err(|_| OdosError::invalid_input("Invalid input amount format"))?;
Ok(U256::from(amount))
}
pub fn out_amount(&self) -> Option<&String> {
self.out_amounts.first()
}
pub fn out_amounts_iter(&self) -> impl Iterator<Item = &String> {
self.out_amounts.iter()
}
pub fn in_tokens_iter(&self) -> impl Iterator<Item = &Address> {
self.in_tokens.iter()
}
pub fn first_in_token(&self) -> Option<&Address> {
self.in_tokens.first()
}
pub fn out_tokens_iter(&self) -> impl Iterator<Item = &Address> {
self.out_tokens.iter()
}
pub fn first_out_token(&self) -> Option<&Address> {
self.out_tokens.first()
}
pub fn out_values_iter(&self) -> impl Iterator<Item = &f64> {
self.out_values.iter()
}
pub fn path_id(&self) -> &str {
&self.path_id
}
pub fn path_definition_as_vec_u8(&self) -> Vec<u8> {
self.path_id().as_bytes().to_vec()
}
pub fn swap_input_token_and_amount(&self) -> Result<(Address, U256)> {
let input_token = *self
.in_tokens_iter()
.next()
.ok_or_else(|| OdosError::missing_data("Missing input token"))?;
let input_amount_in_u256 = self.in_amount_u256()?;
Ok((input_token, input_amount_in_u256))
}
pub fn price_impact(&self) -> f64 {
self.price_impact
}
pub fn net_out_value(&self) -> f64 {
self.net_out_value
}
pub fn partner_fee_percent(&self) -> f64 {
self.partner_fee_percent
}
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct OdosApiErrorResponse {
pub detail: String,
#[serde(default)]
pub trace_id: Option<TraceId>,
pub error_code: u16,
}
#[cfg(feature = "v2")]
#[derive(Clone, Debug)]
pub struct SwapInputs {
executor: Address,
path_definition: Bytes,
input_token_info: inputTokenInfo,
output_token_info: outputTokenInfo,
value_out_min: U256,
}
#[cfg(feature = "v2")]
impl TryFrom<OdosV2RouterCalls> for SwapInputs {
type Error = OdosError;
fn try_from(swap: OdosV2RouterCalls) -> std::result::Result<Self, Self::Error> {
match swap {
OdosV2RouterCalls::swap(call) => {
debug!(
swap_type = "V2Router",
input.token = %call.tokenInfo.inputToken,
input.amount_wei = %call.tokenInfo.inputAmount,
output.token = %call.tokenInfo.outputToken,
output.min_wei = %call.tokenInfo.outputMin,
executor = %call.executor,
"Extracting swap inputs from V2 router call"
);
let swapCall {
executor,
pathDefinition,
referralCode,
tokenInfo,
} = call;
let _referral_code = referralCode;
let swapTokenInfo {
inputToken,
inputAmount,
inputReceiver,
outputMin,
outputQuote,
outputReceiver,
outputToken,
} = tokenInfo;
let _output_quote = outputQuote;
Ok(Self {
executor,
path_definition: pathDefinition,
input_token_info: inputTokenInfo {
tokenAddress: inputToken,
amountIn: inputAmount,
receiver: inputReceiver,
},
output_token_info: outputTokenInfo {
tokenAddress: outputToken,
relativeValue: U256::from(1),
receiver: outputReceiver,
},
value_out_min: outputMin,
})
}
_ => Err(OdosError::invalid_input("Unexpected OdosV2RouterCalls")),
}
}
}
#[cfg(feature = "v3")]
impl TryFrom<OdosV3RouterCalls> for SwapInputs {
type Error = OdosError;
fn try_from(swap: OdosV3RouterCalls) -> std::result::Result<Self, Self::Error> {
match swap {
OdosV3RouterCalls::swap(call) => {
debug!(
swap_type = "V3Router",
input.token = %call.tokenInfo.inputToken,
input.amount_wei = %call.tokenInfo.inputAmount,
output.token = %call.tokenInfo.outputToken,
output.min_wei = %call.tokenInfo.outputMin,
executor = %call.executor,
"Extracting swap inputs from V3 router call"
);
let v3SwapTokenInfo {
inputToken,
inputAmount,
inputReceiver,
outputMin,
outputQuote,
outputReceiver,
outputToken,
} = call.tokenInfo;
let _output_quote = outputQuote;
let _referral_info = call.referralInfo;
Ok(Self {
executor: call.executor,
path_definition: call.pathDefinition,
input_token_info: inputTokenInfo {
tokenAddress: inputToken,
amountIn: inputAmount,
receiver: inputReceiver,
},
output_token_info: outputTokenInfo {
tokenAddress: outputToken,
relativeValue: U256::from(1),
receiver: outputReceiver,
},
value_out_min: outputMin,
})
}
_ => Err(OdosError::invalid_input("Unexpected OdosV3RouterCalls")),
}
}
}
#[cfg(feature = "v2")]
impl SwapInputs {
pub fn executor(&self) -> Address {
self.executor
}
pub fn path_definition(&self) -> &Bytes {
&self.path_definition
}
pub fn token_address(&self) -> Address {
self.input_token_info.tokenAddress
}
pub fn amount_in(&self) -> U256 {
self.input_token_info.amountIn
}
pub fn receiver(&self) -> Address {
self.input_token_info.receiver
}
pub fn relative_value(&self) -> U256 {
self.output_token_info.relativeValue
}
pub fn output_token_address(&self) -> Address {
self.output_token_info.tokenAddress
}
pub fn value_out_min(&self) -> U256 {
self.value_out_min
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_api_host_base_url() {
assert_eq!(ApiHost::Public.base_url().as_str(), "https://api.odos.xyz/");
assert_eq!(
ApiHost::Enterprise.base_url().as_str(),
"https://enterprise-api.odos.xyz/"
);
}
#[test]
fn test_api_version_path() {
assert_eq!(ApiVersion::V2.path(), "v2");
assert_eq!(ApiVersion::V3.path(), "v3");
}
#[test]
fn test_endpoint_constructors() {
let endpoint = Endpoint::public_v2();
assert_eq!(endpoint.host(), ApiHost::Public);
assert_eq!(endpoint.version(), ApiVersion::V2);
let endpoint = Endpoint::public_v3();
assert_eq!(endpoint.host(), ApiHost::Public);
assert_eq!(endpoint.version(), ApiVersion::V3);
let endpoint = Endpoint::enterprise_v2();
assert_eq!(endpoint.host(), ApiHost::Enterprise);
assert_eq!(endpoint.version(), ApiVersion::V2);
let endpoint = Endpoint::enterprise_v3();
assert_eq!(endpoint.host(), ApiHost::Enterprise);
assert_eq!(endpoint.version(), ApiVersion::V3);
let endpoint = Endpoint::new(ApiHost::Public, ApiVersion::V2);
assert_eq!(endpoint.host(), ApiHost::Public);
assert_eq!(endpoint.version(), ApiVersion::V2);
}
#[test]
fn test_endpoint_quote_urls() {
assert_eq!(
Endpoint::public_v2().quote_url().as_str(),
"https://api.odos.xyz/sor/quote/v2"
);
assert_eq!(
Endpoint::public_v3().quote_url().as_str(),
"https://api.odos.xyz/sor/quote/v3"
);
assert_eq!(
Endpoint::enterprise_v2().quote_url().as_str(),
"https://enterprise-api.odos.xyz/sor/quote/v2"
);
assert_eq!(
Endpoint::enterprise_v3().quote_url().as_str(),
"https://enterprise-api.odos.xyz/sor/quote/v3"
);
}
#[test]
fn test_endpoint_assemble_urls() {
assert_eq!(
Endpoint::public_v2().assemble_url().as_str(),
"https://api.odos.xyz/sor/assemble"
);
assert_eq!(
Endpoint::public_v3().assemble_url().as_str(),
"https://api.odos.xyz/sor/assemble"
);
assert_eq!(
Endpoint::enterprise_v2().assemble_url().as_str(),
"https://enterprise-api.odos.xyz/sor/assemble"
);
assert_eq!(
Endpoint::enterprise_v3().assemble_url().as_str(),
"https://enterprise-api.odos.xyz/sor/assemble"
);
}
#[test]
fn test_endpoint_default() {
let endpoint = Endpoint::default();
assert_eq!(endpoint.host(), ApiHost::Public);
assert_eq!(endpoint.version(), ApiVersion::V2);
assert_eq!(
endpoint.quote_url().as_str(),
"https://api.odos.xyz/sor/quote/v2"
);
}
#[test]
fn test_endpoint_equality() {
assert_eq!(
Endpoint::public_v2(),
Endpoint::new(ApiHost::Public, ApiVersion::V2)
);
assert_eq!(
Endpoint::enterprise_v3(),
Endpoint::new(ApiHost::Enterprise, ApiVersion::V3)
);
assert_ne!(Endpoint::public_v2(), Endpoint::public_v3());
assert_ne!(Endpoint::public_v2(), Endpoint::enterprise_v2());
}
#[test]
fn test_odos_api_error_response_accepts_null_trace_id() {
let body = r#"{"detail":"x","traceId":null,"errorCode":2999}"#;
let parsed: OdosApiErrorResponse = serde_json::from_str(body).unwrap();
assert_eq!(parsed.trace_id, None);
assert_eq!(
parsed.error_code,
crate::error_code::OdosErrorCode::AlgoInternal.code()
);
}
#[test]
fn test_odos_api_error_response_accepts_missing_trace_id() {
let body = r#"{"detail":"x","errorCode":2999}"#;
let parsed: OdosApiErrorResponse = serde_json::from_str(body).unwrap();
assert_eq!(parsed.trace_id, None);
assert_eq!(
parsed.error_code,
crate::error_code::OdosErrorCode::AlgoInternal.code()
);
}
#[test]
fn test_odos_api_error_response_accepts_present_trace_id() {
let body =
r#"{"detail":"x","traceId":"10becdc8-a021-4491-8201-a17b657204e0","errorCode":2999}"#;
let parsed: OdosApiErrorResponse = serde_json::from_str(body).unwrap();
assert!(parsed.trace_id.is_some());
assert_eq!(
parsed.error_code,
crate::error_code::OdosErrorCode::AlgoInternal.code()
);
}
}