enso_api/
types.rs

1//! Types for the Enso Finance API
2//!
3//! This module contains request and response types for the Enso Finance API,
4//! including routing, bundling, and position management.
5
6use serde::{Deserialize, Serialize};
7
8/// Supported chains for Enso Finance 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    /// Arbitrum One (chain ID: 42161)
21    Arbitrum,
22    /// Optimism (chain ID: 10)
23    Optimism,
24    /// Base (chain ID: 8453)
25    Base,
26    /// Gnosis Chain (chain ID: 100)
27    Gnosis,
28}
29
30impl Chain {
31    /// Get the chain ID
32    #[must_use]
33    pub const fn chain_id(&self) -> u64 {
34        match self {
35            Self::Ethereum => 1,
36            Self::Polygon => 137,
37            Self::Bsc => 56,
38            Self::Avalanche => 43114,
39            Self::Arbitrum => 42161,
40            Self::Optimism => 10,
41            Self::Base => 8453,
42            Self::Gnosis => 100,
43        }
44    }
45
46    /// Get the chain name as used in the API
47    #[must_use]
48    pub const fn as_str(&self) -> &'static str {
49        match self {
50            Self::Ethereum => "ethereum",
51            Self::Polygon => "polygon",
52            Self::Bsc => "bsc",
53            Self::Avalanche => "avalanche",
54            Self::Arbitrum => "arbitrum",
55            Self::Optimism => "optimism",
56            Self::Base => "base",
57            Self::Gnosis => "gnosis",
58        }
59    }
60
61    /// Parse chain from chain ID
62    #[must_use]
63    pub const fn from_chain_id(id: u64) -> Option<Self> {
64        match id {
65            1 => Some(Self::Ethereum),
66            137 => Some(Self::Polygon),
67            56 => Some(Self::Bsc),
68            43114 => Some(Self::Avalanche),
69            42161 => Some(Self::Arbitrum),
70            10 => Some(Self::Optimism),
71            8453 => Some(Self::Base),
72            100 => Some(Self::Gnosis),
73            _ => None,
74        }
75    }
76}
77
78impl std::fmt::Display for Chain {
79    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80        write!(f, "{}", self.as_str())
81    }
82}
83
84impl TryFrom<yldfi_common::Chain> for Chain {
85    type Error = &'static str;
86
87    fn try_from(chain: yldfi_common::Chain) -> Result<Self, Self::Error> {
88        match chain {
89            yldfi_common::Chain::Ethereum => Ok(Self::Ethereum),
90            yldfi_common::Chain::Polygon => Ok(Self::Polygon),
91            yldfi_common::Chain::Bsc => Ok(Self::Bsc),
92            yldfi_common::Chain::Avalanche => Ok(Self::Avalanche),
93            yldfi_common::Chain::Arbitrum => Ok(Self::Arbitrum),
94            yldfi_common::Chain::Optimism => Ok(Self::Optimism),
95            yldfi_common::Chain::Base => Ok(Self::Base),
96            yldfi_common::Chain::Gnosis => Ok(Self::Gnosis),
97            _ => Err("Chain not supported by Enso Finance API"),
98        }
99    }
100}
101
102impl From<Chain> for yldfi_common::Chain {
103    fn from(chain: Chain) -> Self {
104        match chain {
105            Chain::Ethereum => Self::Ethereum,
106            Chain::Polygon => Self::Polygon,
107            Chain::Bsc => Self::Bsc,
108            Chain::Avalanche => Self::Avalanche,
109            Chain::Arbitrum => Self::Arbitrum,
110            Chain::Optimism => Self::Optimism,
111            Chain::Base => Self::Base,
112            Chain::Gnosis => Self::Gnosis,
113        }
114    }
115}
116
117/// Routing strategy for Enso
118#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
119#[serde(rename_all = "lowercase")]
120pub enum RoutingStrategy {
121    /// Use the router contract directly
122    #[default]
123    Router,
124    /// Use delegate call through smart wallet
125    Delegate,
126    /// Use Enso smart wallet
127    Ensowallet,
128}
129
130/// Route request parameters for getting swap routes
131#[derive(Debug, Clone, Serialize)]
132#[serde(rename_all = "camelCase")]
133pub struct RouteRequest {
134    /// Chain ID
135    pub chain_id: u64,
136    /// Sender/from address
137    pub from_address: String,
138    /// Receiver address
139    pub receiver: String,
140    /// Input token addresses
141    pub token_in: Vec<String>,
142    /// Output token addresses
143    pub token_out: Vec<String>,
144    /// Input amounts (in smallest units)
145    pub amount_in: Vec<String>,
146    /// Slippage tolerance in basis points (as string, e.g., "100" for 1%)
147    pub slippage: String,
148    /// Routing strategy
149    #[serde(skip_serializing_if = "Option::is_none")]
150    pub routing_strategy: Option<RoutingStrategy>,
151    /// Spender address (for approvals)
152    #[serde(skip_serializing_if = "Option::is_none")]
153    pub spender: Option<String>,
154    /// Disable estimate (for faster quotes)
155    #[serde(skip_serializing_if = "Option::is_none")]
156    pub disable_estimate: Option<bool>,
157}
158
159impl RouteRequest {
160    /// Create a new route request for a simple swap
161    #[must_use]
162    pub fn new(
163        chain_id: u64,
164        from_address: impl Into<String>,
165        token_in: impl Into<String>,
166        token_out: impl Into<String>,
167        amount_in: impl Into<String>,
168        slippage_bps: u16,
169    ) -> Self {
170        let from = from_address.into();
171        Self {
172            chain_id,
173            from_address: from.clone(),
174            receiver: from,
175            token_in: vec![token_in.into()],
176            token_out: vec![token_out.into()],
177            amount_in: vec![amount_in.into()],
178            slippage: slippage_bps.to_string(),
179            routing_strategy: None,
180            spender: None,
181            disable_estimate: None,
182        }
183    }
184
185    /// Set a different receiver address
186    #[must_use]
187    pub fn with_receiver(mut self, receiver: impl Into<String>) -> Self {
188        self.receiver = receiver.into();
189        self
190    }
191
192    /// Set the routing strategy
193    #[must_use]
194    pub fn with_routing_strategy(mut self, strategy: RoutingStrategy) -> Self {
195        self.routing_strategy = Some(strategy);
196        self
197    }
198
199    /// Set the spender address
200    #[must_use]
201    pub fn with_spender(mut self, spender: impl Into<String>) -> Self {
202        self.spender = Some(spender.into());
203        self
204    }
205
206    /// Disable estimation for faster quotes
207    #[must_use]
208    pub fn with_disable_estimate(mut self, disable: bool) -> Self {
209        self.disable_estimate = Some(disable);
210        self
211    }
212
213    /// Add multiple input tokens (for multi-input swaps)
214    #[must_use]
215    pub fn with_tokens_in(mut self, tokens: Vec<String>, amounts: Vec<String>) -> Self {
216        self.token_in = tokens;
217        self.amount_in = amounts;
218        self
219    }
220
221    /// Add multiple output tokens (for multi-output swaps)
222    #[must_use]
223    pub fn with_tokens_out(mut self, tokens: Vec<String>) -> Self {
224        self.token_out = tokens;
225        self
226    }
227}
228
229/// Route response with transaction data
230#[derive(Debug, Clone, Deserialize)]
231#[serde(rename_all = "camelCase")]
232pub struct RouteResponse {
233    /// Estimated output amount
234    pub amount_out: String,
235    /// Minimum output amount after slippage
236    #[serde(default, alias = "amount_out_min")]
237    pub min_amount_out: Option<String>,
238    /// Transaction data
239    pub tx: TransactionData,
240    /// Route steps
241    #[serde(default)]
242    pub route: Vec<RouteStep>,
243    /// Estimated gas
244    #[serde(default)]
245    pub gas: Option<String>,
246    /// Price impact percentage (can be 0 integer or 0.0 float)
247    #[serde(default, deserialize_with = "deserialize_price_impact")]
248    pub price_impact: Option<f64>,
249    /// Created timestamp
250    #[serde(default)]
251    pub created_at: Option<u64>,
252}
253
254/// Deserialize price_impact that can be an integer (0) or float (0.0)
255fn deserialize_price_impact<'de, D>(deserializer: D) -> Result<Option<f64>, D::Error>
256where
257    D: serde::Deserializer<'de>,
258{
259    use serde::de::{self, Visitor};
260
261    struct PriceImpactVisitor;
262
263    impl<'de> Visitor<'de> for PriceImpactVisitor {
264        type Value = Option<f64>;
265
266        fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
267            formatter.write_str("a number or null")
268        }
269
270        fn visit_none<E>(self) -> Result<Self::Value, E>
271        where
272            E: de::Error,
273        {
274            Ok(None)
275        }
276
277        fn visit_unit<E>(self) -> Result<Self::Value, E>
278        where
279            E: de::Error,
280        {
281            Ok(None)
282        }
283
284        fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
285        where
286            E: de::Error,
287        {
288            Ok(Some(v as f64))
289        }
290
291        fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
292        where
293            E: de::Error,
294        {
295            Ok(Some(v as f64))
296        }
297
298        fn visit_f64<E>(self, v: f64) -> Result<Self::Value, E>
299        where
300            E: de::Error,
301        {
302            Ok(Some(v))
303        }
304    }
305
306    deserializer.deserialize_any(PriceImpactVisitor)
307}
308
309/// Transaction data for execution
310#[derive(Debug, Clone, Deserialize)]
311#[serde(rename_all = "camelCase")]
312pub struct TransactionData {
313    /// Target contract address
314    pub to: String,
315    /// Sender address
316    pub from: String,
317    /// Encoded calldata
318    pub data: String,
319    /// Value to send (for native token swaps)
320    pub value: String,
321}
322
323/// Route step in the swap path
324#[derive(Debug, Clone, Deserialize)]
325#[serde(rename_all = "camelCase")]
326pub struct RouteStep {
327    /// Protocol/DEX name
328    pub protocol: String,
329    /// Action type
330    #[serde(default)]
331    pub action: Option<String>,
332    /// Token in addresses (can be single or multiple)
333    #[serde(default)]
334    pub token_in: Option<Vec<String>>,
335    /// Token out addresses (can be single or multiple)
336    #[serde(default)]
337    pub token_out: Option<Vec<String>>,
338    /// Amount in
339    #[serde(default)]
340    pub amount_in: Option<String>,
341    /// Amount out
342    #[serde(default)]
343    pub amount_out: Option<String>,
344    /// Portion percentage
345    #[serde(default)]
346    pub portion: Option<u8>,
347    /// Chain ID for the step
348    #[serde(default)]
349    pub chain_id: Option<u64>,
350}
351
352/// Bundle action for multi-step transactions
353#[derive(Debug, Clone, Serialize)]
354#[serde(rename_all = "camelCase")]
355pub struct BundleAction {
356    /// Protocol to interact with
357    pub protocol: String,
358    /// Action type (swap, deposit, withdraw, etc.)
359    pub action: String,
360    /// Action arguments (protocol-specific)
361    pub args: serde_json::Value,
362}
363
364impl BundleAction {
365    /// Create a new bundle action
366    #[must_use]
367    pub fn new(
368        protocol: impl Into<String>,
369        action: impl Into<String>,
370        args: serde_json::Value,
371    ) -> Self {
372        Self {
373            protocol: protocol.into(),
374            action: action.into(),
375            args,
376        }
377    }
378
379    /// Create a swap action
380    #[must_use]
381    pub fn swap(
382        token_in: impl Into<String>,
383        token_out: impl Into<String>,
384        amount_in: impl Into<String>,
385    ) -> Self {
386        Self {
387            protocol: "enso".to_string(),
388            action: "route".to_string(),
389            args: serde_json::json!({
390                "tokenIn": token_in.into(),
391                "tokenOut": token_out.into(),
392                "amountIn": amount_in.into()
393            }),
394        }
395    }
396}
397
398/// Bundle request for multi-action transactions
399#[derive(Debug, Clone, Serialize)]
400#[serde(rename_all = "camelCase")]
401pub struct BundleRequest {
402    /// Chain ID
403    pub chain_id: u64,
404    /// Sender address
405    pub from_address: String,
406    /// Actions to bundle
407    pub actions: Vec<BundleAction>,
408    /// Routing strategy
409    #[serde(skip_serializing_if = "Option::is_none")]
410    pub routing_strategy: Option<RoutingStrategy>,
411}
412
413impl BundleRequest {
414    /// Create a new bundle request
415    #[must_use]
416    pub fn new(chain_id: u64, from_address: impl Into<String>, actions: Vec<BundleAction>) -> Self {
417        Self {
418            chain_id,
419            from_address: from_address.into(),
420            actions,
421            routing_strategy: None,
422        }
423    }
424
425    /// Set the routing strategy
426    #[must_use]
427    pub fn with_routing_strategy(mut self, strategy: RoutingStrategy) -> Self {
428        self.routing_strategy = Some(strategy);
429        self
430    }
431}
432
433/// Bundle response
434#[derive(Debug, Clone, Deserialize)]
435#[serde(rename_all = "camelCase")]
436pub struct BundleResponse {
437    /// Transaction data
438    pub tx: TransactionData,
439    /// Estimated gas
440    #[serde(default)]
441    pub gas: Option<String>,
442    /// Bundle hash/ID
443    #[serde(default)]
444    pub bundle_hash: Option<String>,
445}
446
447/// Token price information
448#[derive(Debug, Clone, Deserialize)]
449#[serde(rename_all = "camelCase")]
450pub struct TokenPrice {
451    /// Token address
452    pub address: String,
453    /// Price in USD
454    pub price: f64,
455    /// Token symbol
456    #[serde(default)]
457    pub symbol: Option<String>,
458    /// Token decimals
459    #[serde(default)]
460    pub decimals: Option<u8>,
461}
462
463/// Token balance information
464#[derive(Debug, Clone, Deserialize)]
465#[serde(rename_all = "camelCase")]
466pub struct TokenBalance {
467    /// Token address
468    pub address: String,
469    /// Balance in smallest units
470    pub balance: String,
471    /// Token symbol
472    #[serde(default)]
473    pub symbol: Option<String>,
474    /// Token decimals
475    #[serde(default)]
476    pub decimals: Option<u8>,
477    /// USD value
478    #[serde(default)]
479    pub usd_value: Option<f64>,
480}
481
482/// API error response
483#[derive(Debug, Clone, Deserialize)]
484pub struct ApiErrorResponse {
485    /// Error message
486    #[serde(alias = "message")]
487    pub error: String,
488    /// Error code
489    #[serde(default)]
490    pub code: Option<String>,
491}
492
493#[cfg(test)]
494mod tests {
495    use super::*;
496
497    #[test]
498    fn test_chain_id() {
499        assert_eq!(Chain::Ethereum.chain_id(), 1);
500        assert_eq!(Chain::Polygon.chain_id(), 137);
501        assert_eq!(Chain::Arbitrum.chain_id(), 42161);
502    }
503
504    #[test]
505    fn test_chain_from_id() {
506        assert_eq!(Chain::from_chain_id(1), Some(Chain::Ethereum));
507        assert_eq!(Chain::from_chain_id(137), Some(Chain::Polygon));
508        assert_eq!(Chain::from_chain_id(999999), None);
509    }
510
511    #[test]
512    fn test_route_request() {
513        let request = RouteRequest::new(
514            1,
515            "0xSender",
516            "0xTokenIn",
517            "0xTokenOut",
518            "1000000000000000000",
519            100,
520        );
521
522        assert_eq!(request.chain_id, 1);
523        assert_eq!(request.slippage, "100");
524        assert_eq!(request.token_in.len(), 1);
525    }
526
527    #[test]
528    fn test_bundle_action() {
529        let action = BundleAction::swap("0xIn", "0xOut", "1000");
530        assert_eq!(action.protocol, "enso");
531        assert_eq!(action.action, "route");
532    }
533}