Skip to main content

vlra/
types.rs

1//! Types for the Velora (`ParaSwap`) API
2//!
3//! This module contains request and response types for the `ParaSwap` API,
4//! including price routing, transaction building, and token lists.
5
6use serde::{Deserialize, Serialize};
7
8/// Supported chains for Velora/ParaSwap API
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
10#[non_exhaustive]
11pub enum Chain {
12    /// Ethereum mainnet (chain ID: 1)
13    Ethereum,
14    /// Polygon (chain ID: 137)
15    Polygon,
16    /// BNB Chain (chain ID: 56)
17    Bsc,
18    /// Avalanche C-Chain (chain ID: 43114)
19    Avalanche,
20    /// Fantom (chain ID: 250)
21    Fantom,
22    /// Arbitrum One (chain ID: 42161)
23    Arbitrum,
24    /// Optimism (chain ID: 10)
25    Optimism,
26    /// Base (chain ID: 8453)
27    Base,
28    /// zkSync Era (chain ID: 324)
29    ZkSync,
30    /// Polygon zkEVM (chain ID: 1101)
31    PolygonZkEvm,
32}
33
34impl Chain {
35    /// Get the chain ID
36    #[must_use]
37    pub const fn chain_id(&self) -> u64 {
38        match self {
39            Self::Ethereum => 1,
40            Self::Polygon => 137,
41            Self::Bsc => 56,
42            Self::Avalanche => 43114,
43            Self::Fantom => 250,
44            Self::Arbitrum => 42161,
45            Self::Optimism => 10,
46            Self::Base => 8453,
47            Self::ZkSync => 324,
48            Self::PolygonZkEvm => 1101,
49        }
50    }
51
52    /// Get the chain name as used in the API
53    #[must_use]
54    pub const fn as_str(&self) -> &'static str {
55        match self {
56            Self::Ethereum => "ethereum",
57            Self::Polygon => "polygon",
58            Self::Bsc => "bsc",
59            Self::Avalanche => "avalanche",
60            Self::Fantom => "fantom",
61            Self::Arbitrum => "arbitrum",
62            Self::Optimism => "optimism",
63            Self::Base => "base",
64            Self::ZkSync => "zksync",
65            Self::PolygonZkEvm => "polygon-zkevm",
66        }
67    }
68
69    /// Parse chain from chain ID
70    #[must_use]
71    pub const fn from_chain_id(id: u64) -> Option<Self> {
72        match id {
73            1 => Some(Self::Ethereum),
74            137 => Some(Self::Polygon),
75            56 => Some(Self::Bsc),
76            43114 => Some(Self::Avalanche),
77            250 => Some(Self::Fantom),
78            42161 => Some(Self::Arbitrum),
79            10 => Some(Self::Optimism),
80            8453 => Some(Self::Base),
81            324 => Some(Self::ZkSync),
82            1101 => Some(Self::PolygonZkEvm),
83            _ => None,
84        }
85    }
86}
87
88impl std::fmt::Display for Chain {
89    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
90        write!(f, "{}", self.as_str())
91    }
92}
93
94impl TryFrom<yldfi_common::Chain> for Chain {
95    type Error = &'static str;
96
97    fn try_from(chain: yldfi_common::Chain) -> Result<Self, Self::Error> {
98        match chain {
99            yldfi_common::Chain::Ethereum => Ok(Self::Ethereum),
100            yldfi_common::Chain::Polygon => Ok(Self::Polygon),
101            yldfi_common::Chain::Bsc => Ok(Self::Bsc),
102            yldfi_common::Chain::Avalanche => Ok(Self::Avalanche),
103            yldfi_common::Chain::Fantom => Ok(Self::Fantom),
104            yldfi_common::Chain::Arbitrum => Ok(Self::Arbitrum),
105            yldfi_common::Chain::Optimism => Ok(Self::Optimism),
106            yldfi_common::Chain::Base => Ok(Self::Base),
107            yldfi_common::Chain::ZkSync => Ok(Self::ZkSync),
108            yldfi_common::Chain::PolygonZkEvm => Ok(Self::PolygonZkEvm),
109            _ => Err("Chain not supported by Velora/ParaSwap API"),
110        }
111    }
112}
113
114impl From<Chain> for yldfi_common::Chain {
115    fn from(chain: Chain) -> Self {
116        match chain {
117            Chain::Ethereum => Self::Ethereum,
118            Chain::Polygon => Self::Polygon,
119            Chain::Bsc => Self::Bsc,
120            Chain::Avalanche => Self::Avalanche,
121            Chain::Fantom => Self::Fantom,
122            Chain::Arbitrum => Self::Arbitrum,
123            Chain::Optimism => Self::Optimism,
124            Chain::Base => Self::Base,
125            Chain::ZkSync => Self::ZkSync,
126            Chain::PolygonZkEvm => Self::PolygonZkEvm,
127        }
128    }
129}
130
131/// Side of the swap (SELL or BUY)
132#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
133#[serde(rename_all = "UPPERCASE")]
134pub enum Side {
135    /// Sell a specific amount of source token
136    #[default]
137    Sell,
138    /// Buy a specific amount of destination token
139    Buy,
140}
141
142/// Price request parameters for getting swap quotes
143#[derive(Debug, Clone, Default)]
144pub struct PriceRequest {
145    /// Address of the source token
146    pub src_token: String,
147    /// Address of the destination token
148    pub dest_token: String,
149    /// Amount to swap (in source token's smallest unit)
150    pub amount: String,
151    /// Side of the swap (SELL or BUY)
152    pub side: Side,
153    /// Decimals of source token (optional, improves accuracy)
154    pub src_decimals: Option<u8>,
155    /// Decimals of destination token (optional, improves accuracy)
156    pub dest_decimals: Option<u8>,
157    /// User address (optional, enables more accurate routing)
158    pub user_address: Option<String>,
159    /// Partner address for referral fees
160    pub partner: Option<String>,
161    /// Exclude specific DEXs from the route
162    pub exclude_dexs: Option<String>,
163    /// Include only specific DEXs in the route
164    pub include_dexs: Option<String>,
165    /// Exclude pools with low TVL
166    pub exclude_pools_with_low_tvl: Option<bool>,
167}
168
169impl PriceRequest {
170    /// Create a new price request for selling tokens
171    #[must_use]
172    pub fn sell(
173        src_token: impl Into<String>,
174        dest_token: impl Into<String>,
175        amount: impl Into<String>,
176    ) -> Self {
177        Self {
178            src_token: src_token.into(),
179            dest_token: dest_token.into(),
180            amount: amount.into(),
181            side: Side::Sell,
182            ..Default::default()
183        }
184    }
185
186    /// Create a new price request for buying tokens
187    #[must_use]
188    pub fn buy(
189        src_token: impl Into<String>,
190        dest_token: impl Into<String>,
191        amount: impl Into<String>,
192    ) -> Self {
193        Self {
194            src_token: src_token.into(),
195            dest_token: dest_token.into(),
196            amount: amount.into(),
197            side: Side::Buy,
198            ..Default::default()
199        }
200    }
201
202    /// Set source token decimals
203    #[must_use]
204    pub fn with_src_decimals(mut self, decimals: u8) -> Self {
205        self.src_decimals = Some(decimals);
206        self
207    }
208
209    /// Set destination token decimals
210    #[must_use]
211    pub fn with_dest_decimals(mut self, decimals: u8) -> Self {
212        self.dest_decimals = Some(decimals);
213        self
214    }
215
216    /// Set user address
217    #[must_use]
218    pub fn with_user_address(mut self, address: impl Into<String>) -> Self {
219        self.user_address = Some(address.into());
220        self
221    }
222
223    /// Set partner address for referral
224    #[must_use]
225    pub fn with_partner(mut self, partner: impl Into<String>) -> Self {
226        self.partner = Some(partner.into());
227        self
228    }
229
230    /// Exclude specific DEXs
231    #[must_use]
232    pub fn with_exclude_dexs(mut self, dexs: impl Into<String>) -> Self {
233        self.exclude_dexs = Some(dexs.into());
234        self
235    }
236
237    /// Convert to query parameters
238    #[must_use]
239    pub fn to_query_params(&self, network: u64) -> Vec<(String, String)> {
240        let mut params = vec![
241            ("srcToken".to_string(), self.src_token.clone()),
242            ("destToken".to_string(), self.dest_token.clone()),
243            ("amount".to_string(), self.amount.clone()),
244            (
245                "side".to_string(),
246                match self.side {
247                    Side::Sell => "SELL".to_string(),
248                    Side::Buy => "BUY".to_string(),
249                },
250            ),
251            ("network".to_string(), network.to_string()),
252        ];
253
254        if let Some(decimals) = self.src_decimals {
255            params.push(("srcDecimals".to_string(), decimals.to_string()));
256        }
257        if let Some(decimals) = self.dest_decimals {
258            params.push(("destDecimals".to_string(), decimals.to_string()));
259        }
260        if let Some(ref addr) = self.user_address {
261            params.push(("userAddress".to_string(), addr.clone()));
262        }
263        if let Some(ref partner) = self.partner {
264            params.push(("partner".to_string(), partner.clone()));
265        }
266        if let Some(ref dexs) = self.exclude_dexs {
267            params.push(("excludeDEXS".to_string(), dexs.clone()));
268        }
269        if let Some(ref dexs) = self.include_dexs {
270            params.push(("includeDEXS".to_string(), dexs.clone()));
271        }
272        if let Some(exclude) = self.exclude_pools_with_low_tvl {
273            params.push(("excludePoolsWithLowTVL".to_string(), exclude.to_string()));
274        }
275
276        params
277    }
278}
279
280/// Price response with routing information
281#[derive(Debug, Clone, Serialize, Deserialize)]
282#[serde(rename_all = "camelCase")]
283pub struct PriceResponse {
284    /// Price route containing all swap details
285    pub price_route: PriceRoute,
286}
287
288/// Detailed price route information
289#[derive(Debug, Clone, Deserialize, Serialize)]
290#[serde(rename_all = "camelCase")]
291pub struct PriceRoute {
292    /// Block number when the quote was generated
293    pub block_number: u64,
294    /// Network/chain ID
295    pub network: u64,
296    /// Source token address
297    pub src_token: String,
298    /// Source token decimals
299    pub src_decimals: u8,
300    /// Source amount in smallest units
301    pub src_amount: String,
302    /// Destination token address
303    pub dest_token: String,
304    /// Destination token decimals
305    pub dest_decimals: u8,
306    /// Destination amount in smallest units
307    pub dest_amount: String,
308    /// Best route details
309    pub best_route: Vec<Route>,
310    /// Token transfer proxy address (for approvals)
311    pub token_transfer_proxy: String,
312    /// Contract address for the swap
313    pub contract_address: String,
314    /// Contract method to call
315    pub contract_method: String,
316    /// Partner fee percentage (if applicable)
317    #[serde(default)]
318    pub partner_fee: f64,
319    /// Estimated gas cost
320    pub gas_cost: Option<String>,
321    /// Gas cost in USD
322    pub gas_cost_usd: Option<String>,
323    /// Side of the swap
324    pub side: String,
325    /// Source token USD value
326    pub src_usd: Option<String>,
327    /// Destination token USD value
328    pub dest_usd: Option<String>,
329    /// Max impact percentage
330    pub max_impact_reached: Option<bool>,
331    /// Price impact percentage
332    #[serde(default)]
333    pub price_impact: Option<String>,
334}
335
336/// Route segment in a multi-hop swap
337#[derive(Debug, Clone, Deserialize, Serialize)]
338#[serde(rename_all = "camelCase")]
339pub struct Route {
340    /// Percentage of the swap going through this route
341    pub percent: f64,
342    /// Swap steps in this route
343    pub swaps: Vec<Swap>,
344}
345
346/// Individual swap step within a route
347#[derive(Debug, Clone, Deserialize, Serialize)]
348#[serde(rename_all = "camelCase")]
349pub struct Swap {
350    /// Source token address
351    pub src_token: String,
352    /// Source token decimals
353    pub src_decimals: u8,
354    /// Destination token address
355    pub dest_token: String,
356    /// Destination token decimals
357    pub dest_decimals: u8,
358    /// Pool addresses used in this swap
359    pub swap_exchanges: Vec<SwapExchange>,
360}
361
362/// Exchange details for a swap step
363#[derive(Debug, Clone, Deserialize, Serialize)]
364#[serde(rename_all = "camelCase")]
365pub struct SwapExchange {
366    /// Exchange/DEX name
367    pub exchange: String,
368    /// Source amount
369    pub src_amount: String,
370    /// Destination amount
371    pub dest_amount: String,
372    /// Percentage of the swap
373    pub percent: f64,
374    /// Pool addresses
375    #[serde(default)]
376    pub pool_addresses: Vec<String>,
377    /// Additional exchange data
378    #[serde(default)]
379    pub data: Option<serde_json::Value>,
380}
381
382/// Transaction build request
383#[derive(Debug, Clone, Serialize)]
384#[serde(rename_all = "camelCase")]
385pub struct TransactionRequest {
386    /// Source token address
387    pub src_token: String,
388    /// Destination token address
389    pub dest_token: String,
390    /// Source amount
391    pub src_amount: String,
392    /// Destination amount (from price route)
393    pub dest_amount: String,
394    /// Price route from `PriceResponse`
395    pub price_route: serde_json::Value,
396    /// Slippage tolerance in basis points (e.g., 100 = 1%)
397    pub slippage: u32,
398    /// User address executing the swap
399    pub user_address: String,
400    /// Partner address for referral
401    #[serde(skip_serializing_if = "Option::is_none")]
402    pub partner: Option<String>,
403    /// Receiver address (if different from user)
404    #[serde(skip_serializing_if = "Option::is_none")]
405    pub receiver: Option<String>,
406    /// Deadline timestamp (optional)
407    #[serde(skip_serializing_if = "Option::is_none")]
408    pub deadline: Option<String>,
409    /// Permit data for gasless approvals
410    #[serde(skip_serializing_if = "Option::is_none")]
411    pub permit: Option<String>,
412    /// Ignore gas estimation
413    #[serde(skip_serializing_if = "Option::is_none")]
414    pub ignore_gas: Option<bool>,
415    /// Ignore balance and allowance checks
416    #[serde(skip_serializing_if = "Option::is_none")]
417    pub ignore_checks: Option<bool>,
418}
419
420impl TransactionRequest {
421    /// Create a new transaction request from a price route
422    #[must_use]
423    pub fn new(price_route: &PriceRoute, user_address: impl Into<String>, slippage: u32) -> Self {
424        Self {
425            src_token: price_route.src_token.clone(),
426            dest_token: price_route.dest_token.clone(),
427            src_amount: price_route.src_amount.clone(),
428            dest_amount: price_route.dest_amount.clone(),
429            price_route: serde_json::to_value(price_route).unwrap_or_default(),
430            slippage,
431            user_address: user_address.into(),
432            partner: None,
433            receiver: None,
434            deadline: None,
435            permit: None,
436            ignore_gas: None,
437            ignore_checks: None,
438        }
439    }
440
441    /// Set receiver address
442    #[must_use]
443    pub fn with_receiver(mut self, receiver: impl Into<String>) -> Self {
444        self.receiver = Some(receiver.into());
445        self
446    }
447
448    /// Set partner address
449    #[must_use]
450    pub fn with_partner(mut self, partner: impl Into<String>) -> Self {
451        self.partner = Some(partner.into());
452        self
453    }
454
455    /// Set deadline timestamp
456    #[must_use]
457    pub fn with_deadline(mut self, deadline: impl Into<String>) -> Self {
458        self.deadline = Some(deadline.into());
459        self
460    }
461
462    /// Ignore gas estimation
463    #[must_use]
464    pub fn with_ignore_gas(mut self, ignore: bool) -> Self {
465        self.ignore_gas = Some(ignore);
466        self
467    }
468
469    /// Ignore balance and allowance checks
470    #[must_use]
471    pub fn with_ignore_checks(mut self, ignore: bool) -> Self {
472        self.ignore_checks = Some(ignore);
473        self
474    }
475}
476
477/// Transaction response ready for signing
478#[derive(Debug, Clone, Serialize, Deserialize)]
479#[serde(rename_all = "camelCase")]
480pub struct TransactionResponse {
481    /// Sender address
482    pub from: String,
483    /// Router contract address
484    pub to: String,
485    /// Chain ID
486    pub chain_id: u64,
487    /// Value to send (for native token swaps)
488    pub value: String,
489    /// Encoded transaction data
490    pub data: String,
491    /// Suggested gas price
492    #[serde(default)]
493    pub gas_price: Option<String>,
494    /// Estimated gas limit
495    #[serde(default)]
496    pub gas: Option<String>,
497}
498
499/// Token information
500#[derive(Debug, Clone, Serialize, Deserialize)]
501#[serde(rename_all = "camelCase")]
502pub struct Token {
503    /// Token contract address
504    pub address: String,
505    /// Token symbol
506    pub symbol: String,
507    /// Token name (optional, not always returned by API)
508    #[serde(default)]
509    pub name: Option<String>,
510    /// Token decimals
511    pub decimals: u8,
512    /// Token logo URL
513    #[serde(default)]
514    pub img: Option<String>,
515    /// Is native token
516    #[serde(default)]
517    pub is_native: Option<bool>,
518}
519
520/// Token list response
521#[derive(Debug, Clone, Serialize, Deserialize)]
522pub struct TokenListResponse {
523    /// List of tokens
524    pub tokens: Vec<Token>,
525}
526
527/// API error response
528#[derive(Debug, Clone, Deserialize)]
529pub struct ApiErrorResponse {
530    /// Error message
531    pub error: String,
532}
533
534#[cfg(test)]
535mod tests {
536    use super::*;
537
538    #[test]
539    fn test_chain_id() {
540        assert_eq!(Chain::Ethereum.chain_id(), 1);
541        assert_eq!(Chain::Polygon.chain_id(), 137);
542        assert_eq!(Chain::Arbitrum.chain_id(), 42161);
543    }
544
545    #[test]
546    fn test_chain_from_id() {
547        assert_eq!(Chain::from_chain_id(1), Some(Chain::Ethereum));
548        assert_eq!(Chain::from_chain_id(137), Some(Chain::Polygon));
549        assert_eq!(Chain::from_chain_id(999999), None);
550    }
551
552    #[test]
553    fn test_price_request() {
554        let request = PriceRequest::sell(
555            "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
556            "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
557            "1000000000000000000",
558        )
559        .with_src_decimals(18)
560        .with_dest_decimals(6);
561
562        assert_eq!(request.side, Side::Sell);
563        assert_eq!(request.src_decimals, Some(18));
564        assert_eq!(request.dest_decimals, Some(6));
565    }
566}