1use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
10#[non_exhaustive]
11pub enum Chain {
12 Ethereum,
14 Polygon,
16 Bsc,
18 Avalanche,
20 Arbitrum,
22 Optimism,
24 Base,
26 Gnosis,
28}
29
30impl Chain {
31 #[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 #[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 #[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#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
119#[serde(rename_all = "lowercase")]
120pub enum RoutingStrategy {
121 #[default]
123 Router,
124 Delegate,
126 Ensowallet,
128}
129
130#[derive(Debug, Clone, Serialize)]
132#[serde(rename_all = "camelCase")]
133pub struct RouteRequest {
134 pub chain_id: u64,
136 pub from_address: String,
138 pub receiver: String,
140 pub token_in: Vec<String>,
142 pub token_out: Vec<String>,
144 pub amount_in: Vec<String>,
146 pub slippage: String,
148 #[serde(skip_serializing_if = "Option::is_none")]
150 pub routing_strategy: Option<RoutingStrategy>,
151 #[serde(skip_serializing_if = "Option::is_none")]
153 pub spender: Option<String>,
154 #[serde(skip_serializing_if = "Option::is_none")]
156 pub disable_estimate: Option<bool>,
157}
158
159impl RouteRequest {
160 #[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 #[must_use]
187 pub fn with_receiver(mut self, receiver: impl Into<String>) -> Self {
188 self.receiver = receiver.into();
189 self
190 }
191
192 #[must_use]
194 pub fn with_routing_strategy(mut self, strategy: RoutingStrategy) -> Self {
195 self.routing_strategy = Some(strategy);
196 self
197 }
198
199 #[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 #[must_use]
208 pub fn with_disable_estimate(mut self, disable: bool) -> Self {
209 self.disable_estimate = Some(disable);
210 self
211 }
212
213 #[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 #[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#[derive(Debug, Clone, Deserialize)]
231#[serde(rename_all = "camelCase")]
232pub struct RouteResponse {
233 pub amount_out: String,
235 #[serde(default, alias = "amount_out_min")]
237 pub min_amount_out: Option<String>,
238 pub tx: TransactionData,
240 #[serde(default)]
242 pub route: Vec<RouteStep>,
243 #[serde(default)]
245 pub gas: Option<String>,
246 #[serde(default, deserialize_with = "deserialize_price_impact")]
248 pub price_impact: Option<f64>,
249 #[serde(default)]
251 pub created_at: Option<u64>,
252}
253
254fn 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#[derive(Debug, Clone, Deserialize)]
311#[serde(rename_all = "camelCase")]
312pub struct TransactionData {
313 pub to: String,
315 pub from: String,
317 pub data: String,
319 pub value: String,
321}
322
323#[derive(Debug, Clone, Deserialize)]
325#[serde(rename_all = "camelCase")]
326pub struct RouteStep {
327 pub protocol: String,
329 #[serde(default)]
331 pub action: Option<String>,
332 #[serde(default)]
334 pub token_in: Option<Vec<String>>,
335 #[serde(default)]
337 pub token_out: Option<Vec<String>>,
338 #[serde(default)]
340 pub amount_in: Option<String>,
341 #[serde(default)]
343 pub amount_out: Option<String>,
344 #[serde(default)]
346 pub portion: Option<u8>,
347 #[serde(default)]
349 pub chain_id: Option<u64>,
350}
351
352#[derive(Debug, Clone, Serialize)]
354#[serde(rename_all = "camelCase")]
355pub struct BundleAction {
356 pub protocol: String,
358 pub action: String,
360 pub args: serde_json::Value,
362}
363
364impl BundleAction {
365 #[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 #[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#[derive(Debug, Clone, Serialize)]
400#[serde(rename_all = "camelCase")]
401pub struct BundleRequest {
402 pub chain_id: u64,
404 pub from_address: String,
406 pub actions: Vec<BundleAction>,
408 #[serde(skip_serializing_if = "Option::is_none")]
410 pub routing_strategy: Option<RoutingStrategy>,
411}
412
413impl BundleRequest {
414 #[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 #[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#[derive(Debug, Clone, Deserialize)]
435#[serde(rename_all = "camelCase")]
436pub struct BundleResponse {
437 pub tx: TransactionData,
439 #[serde(default)]
441 pub gas: Option<String>,
442 #[serde(default)]
444 pub bundle_hash: Option<String>,
445}
446
447#[derive(Debug, Clone, Deserialize)]
449#[serde(rename_all = "camelCase")]
450pub struct TokenPrice {
451 pub address: String,
453 pub price: f64,
455 #[serde(default)]
457 pub symbol: Option<String>,
458 #[serde(default)]
460 pub decimals: Option<u8>,
461}
462
463#[derive(Debug, Clone, Deserialize)]
465#[serde(rename_all = "camelCase")]
466pub struct TokenBalance {
467 pub address: String,
469 pub balance: String,
471 #[serde(default)]
473 pub symbol: Option<String>,
474 #[serde(default)]
476 pub decimals: Option<u8>,
477 #[serde(default)]
479 pub usd_value: Option<f64>,
480}
481
482#[derive(Debug, Clone, Deserialize)]
484pub struct ApiErrorResponse {
485 #[serde(alias = "message")]
487 pub error: String,
488 #[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}