1use fynd_rpc_types as dto;
7use fynd_rpc_types::OrderQuote;
8use tycho_simulation::tycho_common::models::Address as TychoAddress;
9
10use crate::{
11 error::{ErrorCode, FyndError},
12 types::{
13 BackendKind, BatchQuote, BlockInfo, EncodingOptions, FeeBreakdown, HealthStatus, Order,
14 OrderSide, PermitDetails, PermitSingle, Quote, QuoteOptions, QuoteParams, QuoteStatus,
15 Route, Swap, Transaction, UserTransferType,
16 },
17};
18pub(crate) fn bytes_to_alloy_address(
23 b: &bytes::Bytes,
24) -> Result<alloy::primitives::Address, FyndError> {
25 let arr: [u8; 20] = b.as_ref().try_into().map_err(|_| {
26 FyndError::Protocol(format!("expected 20-byte address, got {} bytes", b.len()))
27 })?;
28
29 Ok(alloy::primitives::Address::from(arr))
30}
31
32fn bytes_to_tycho(b: &bytes::Bytes) -> Result<TychoAddress, FyndError> {
34 if b.len() != 20 {
35 return Err(FyndError::Protocol(format!("expected 20-byte address, got {} bytes", b.len())));
36 }
37 Ok(TychoAddress::from(b.clone()))
39}
40
41fn tycho_to_bytes(addr: TychoAddress) -> bytes::Bytes {
43 addr.0
45}
46
47pub(crate) fn biguint_to_u256(n: &num_bigint::BigUint) -> alloy::primitives::U256 {
53 alloy::primitives::U256::from_be_slice(&n.to_bytes_be())
54}
55
56pub(crate) fn quote_params_to_dto(params: QuoteParams) -> Result<dto::QuoteRequest, FyndError> {
61 let order = dto::Order::try_from(params.order)?;
62 let options = dto::QuoteOptions::try_from(params.options)?;
63 Ok(dto::QuoteRequest::new(vec![order]).with_options(options))
64}
65
66impl TryFrom<Order> for dto::Order {
67 type Error = FyndError;
68
69 fn try_from(order: Order) -> Result<Self, Self::Error> {
70 let token_in = bytes_to_tycho(order.token_in())?;
71 let token_out = bytes_to_tycho(order.token_out())?;
72 let sender = bytes_to_tycho(order.sender())?;
73 let receiver = order
74 .receiver()
75 .map(bytes_to_tycho)
76 .transpose()?;
77 let mut dto_order = dto::Order::new(
78 token_in,
79 token_out,
80 order.amount().clone(),
81 order.side().into(),
82 sender,
83 );
84 if let Some(r) = receiver {
85 dto_order = dto_order.with_receiver(r);
86 }
87 Ok(dto_order)
88 }
89}
90
91impl From<OrderSide> for dto::OrderSide {
92 fn from(side: OrderSide) -> Self {
93 match side {
94 OrderSide::Sell => dto::OrderSide::Sell,
95 }
96 }
97}
98
99impl TryFrom<QuoteOptions> for dto::QuoteOptions {
100 type Error = FyndError;
101
102 fn try_from(opts: QuoteOptions) -> Result<Self, Self::Error> {
103 let mut dto_opts = dto::QuoteOptions::default();
104 if let Some(ms) = opts.timeout_ms {
105 dto_opts = dto_opts.with_timeout_ms(ms);
106 }
107 if let Some(n) = opts.min_responses {
108 dto_opts = dto_opts.with_min_responses(n);
109 }
110 if let Some(gas) = opts.max_gas {
111 dto_opts = dto_opts.with_max_gas(gas);
112 }
113 if let Some(enc) = opts.encoding_options {
114 dto_opts = dto_opts.with_encoding_options(dto::EncodingOptions::try_from(enc)?);
115 }
116 Ok(dto_opts)
117 }
118}
119
120impl TryFrom<EncodingOptions> for dto::EncodingOptions {
121 type Error = FyndError;
122
123 fn try_from(opts: EncodingOptions) -> Result<Self, Self::Error> {
124 use tycho_simulation::tycho_core::Bytes as TychoBytes;
125 let mut dto_opts =
126 dto::EncodingOptions::new(opts.slippage).with_transfer_type(opts.transfer_type.into());
127 if let (Some(permit), Some(sig)) = (
128 opts.permit
129 .map(dto::PermitSingle::try_from)
130 .transpose()?,
131 opts.permit2_signature
132 .map(TychoBytes::from),
133 ) {
134 dto_opts = dto_opts.with_permit2(permit, sig);
135 }
136 if let Some(fee) = opts.client_fee_params {
137 dto_opts = dto_opts.with_client_fee_params(dto::ClientFeeParams::new(
138 fee.bps,
139 TychoBytes::from(fee.receiver),
140 fee.max_contribution,
141 fee.deadline,
142 TychoBytes::from(fee.signature.unwrap_or_default()),
143 ));
144 }
145 Ok(dto_opts)
146 }
147}
148
149impl TryFrom<PermitSingle> for dto::PermitSingle {
150 type Error = FyndError;
151
152 fn try_from(p: PermitSingle) -> Result<Self, Self::Error> {
153 use tycho_simulation::tycho_core::Bytes as TychoBytes;
154 let details = dto::PermitDetails::try_from(p.details)?;
155 let spender = TychoBytes::from(bytes_to_tycho(&p.spender)?.0);
156 Ok(dto::PermitSingle::new(details, spender, p.sig_deadline))
157 }
158}
159
160impl TryFrom<PermitDetails> for dto::PermitDetails {
161 type Error = FyndError;
162
163 fn try_from(d: PermitDetails) -> Result<Self, Self::Error> {
164 use tycho_simulation::tycho_core::Bytes as TychoBytes;
165 let token = TychoBytes::from(bytes_to_tycho(&d.token)?.0);
166 Ok(dto::PermitDetails::new(token, d.amount, d.expiration, d.nonce))
167 }
168}
169
170impl From<UserTransferType> for dto::UserTransferType {
171 fn from(t: UserTransferType) -> Self {
172 match t {
173 UserTransferType::TransferFrom => dto::UserTransferType::TransferFrom,
174 UserTransferType::TransferFromPermit2 => dto::UserTransferType::TransferFromPermit2,
175 UserTransferType::UseVaultsFunds => dto::UserTransferType::UseVaultsFunds,
176 }
177 }
178}
179
180pub(crate) fn dto_to_quote(
185 ds: OrderQuote,
186 token_out: bytes::Bytes,
187 receiver: bytes::Bytes,
188) -> Result<Quote, FyndError> {
189 let status = QuoteStatus::from(ds.status());
190 let route = ds
191 .route()
192 .cloned()
193 .map(Route::try_from)
194 .transpose()?;
195 let block = BlockInfo::from(ds.block().clone());
196 let transaction = ds
197 .transaction()
198 .cloned()
199 .map(Transaction::from);
200 let fee_breakdown = ds.fee_breakdown().map(|fb| {
201 FeeBreakdown::new(
202 fb.router_fee().clone(),
203 fb.client_fee().clone(),
204 fb.max_slippage().clone(),
205 fb.min_amount_received().clone(),
206 )
207 });
208 Ok(Quote::new(
209 ds.order_id().to_string(),
210 status,
211 BackendKind::Fynd,
212 route,
213 ds.amount_in().clone(),
214 ds.amount_out().clone(),
215 ds.gas_estimate().clone(),
216 ds.amount_out_net_gas().clone(),
217 ds.price_impact_bps(),
218 block,
219 token_out,
220 receiver,
221 transaction,
222 fee_breakdown,
223 ))
224}
225
226impl From<dto::Transaction> for Transaction {
227 fn from(dt: dto::Transaction) -> Self {
228 Transaction::new(
229 bytes::Bytes::copy_from_slice(dt.to().as_ref()),
230 dt.value().clone(),
231 dt.data().to_vec(),
232 )
233 }
234}
235
236pub(crate) fn dto_to_batch_quote(
237 ds: dto::Quote,
238 token_out: bytes::Bytes,
239 receiver: bytes::Bytes,
240) -> Result<BatchQuote, FyndError> {
241 let quotes = ds
242 .into_orders()
243 .into_iter()
244 .map(|os| dto_to_quote(os, token_out.clone(), receiver.clone()))
245 .collect::<Result<Vec<Quote>, _>>()?;
246 Ok(BatchQuote::new(quotes))
247}
248
249impl From<dto::QuoteStatus> for QuoteStatus {
250 fn from(ds: dto::QuoteStatus) -> Self {
251 match ds {
252 dto::QuoteStatus::Success => Self::Success,
253 dto::QuoteStatus::NoRouteFound => Self::NoRouteFound,
254 dto::QuoteStatus::InsufficientLiquidity => Self::InsufficientLiquidity,
255 dto::QuoteStatus::Timeout => Self::Timeout,
256 dto::QuoteStatus::NotReady => Self::NotReady,
257 _ => unreachable!("unexpected QuoteStatus variant"),
258 }
259 }
260}
261
262impl TryFrom<dto::Route> for Route {
263 type Error = FyndError;
264
265 fn try_from(dr: dto::Route) -> Result<Self, Self::Error> {
266 let swaps = dr
267 .into_swaps()
268 .into_iter()
269 .map(Swap::try_from)
270 .collect::<Result<Vec<_>, _>>()?;
271 Ok(Route::new(swaps))
272 }
273}
274
275impl TryFrom<dto::Swap> for Swap {
276 type Error = FyndError;
277
278 fn try_from(ds: dto::Swap) -> Result<Self, Self::Error> {
279 let token_in = tycho_to_bytes(ds.token_in().clone());
280 let token_out = tycho_to_bytes(ds.token_out().clone());
281 Ok(Swap::new(
282 ds.component_id().to_string(),
283 ds.protocol().to_string(),
284 token_in,
285 token_out,
286 ds.amount_in().clone(),
287 ds.amount_out().clone(),
288 ds.gas_estimate().clone(),
289 ds.split(),
290 ))
291 }
292}
293
294impl From<dto::BlockInfo> for BlockInfo {
295 fn from(db: dto::BlockInfo) -> Self {
296 BlockInfo::new(db.number(), db.hash().to_string(), db.timestamp())
297 }
298}
299
300impl TryFrom<fynd_rpc_types::InstanceInfo> for crate::types::InstanceInfo {
301 type Error = FyndError;
302
303 fn try_from(dto: fynd_rpc_types::InstanceInfo) -> Result<Self, Self::Error> {
304 let router = bytes::Bytes::copy_from_slice(dto.router_address().as_ref());
305 let permit2 = bytes::Bytes::copy_from_slice(dto.permit2_address().as_ref());
306 if router.len() != 20 {
307 return Err(FyndError::Protocol(format!(
308 "router_address must be 20 bytes, got {}",
309 router.len()
310 )));
311 }
312 if permit2.len() != 20 {
313 return Err(FyndError::Protocol(format!(
314 "permit2_address must be 20 bytes, got {}",
315 permit2.len()
316 )));
317 }
318 Ok(crate::types::InstanceInfo::new(router, permit2, dto.chain_id()))
319 }
320}
321
322impl From<dto::HealthStatus> for HealthStatus {
323 fn from(dh: dto::HealthStatus) -> Self {
324 HealthStatus::new(
325 dh.healthy(),
326 dh.last_update_ms(),
327 dh.num_solver_pools(),
328 dh.derived_data_ready(),
329 dh.gas_price_age_ms(),
330 )
331 }
332}
333
334pub(crate) fn dto_error_to_fynd(de: dto::ErrorResponse) -> FyndError {
339 let code = ErrorCode::from_server_code(de.code());
340 FyndError::Api { code, message: de.error().to_string() }
341}
342
343#[cfg(test)]
344mod tests {
345 use bytes::Bytes;
346 use num_bigint::BigUint;
347
348 use super::*;
349
350 fn sample_dto_swap() -> dto::Swap {
351 serde_json::from_str(
352 r#"{
353 "component_id": "pool-1",
354 "protocol": "uniswap-v3",
355 "token_in": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
356 "token_out": "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
357 "amount_in": "100",
358 "amount_out": "99",
359 "gas_estimate": "50000",
360 "split": "0"
361 }"#,
362 )
363 .expect("valid swap JSON")
364 }
365
366 fn sample_dto_block() -> dto::BlockInfo {
367 serde_json::from_str(
368 r#"{
369 "number": 21000000,
370 "hash": "0xdeadbeef",
371 "timestamp": 1730000000
372 }"#,
373 )
374 .expect("valid block JSON")
375 }
376
377 fn sample_dto_order_quote() -> dto::OrderQuote {
378 serde_json::from_str(
379 r#"{
380 "order_id": "test-order-id",
381 "status": "success",
382 "amount_in": "1000",
383 "amount_out": "999",
384 "gas_estimate": "100000",
385 "price_impact_bps": 5,
386 "amount_out_net_gas": "998",
387 "block": {"number": 21000000, "hash": "0xdeadbeef", "timestamp": 1730000000}
388 }"#,
389 )
390 .expect("valid order quote JSON")
391 }
392
393 #[test]
398 fn biguint_to_u256_zero() {
399 let result = biguint_to_u256(&BigUint::ZERO);
400 assert_eq!(result, alloy::primitives::U256::ZERO);
401 }
402
403 #[test]
404 fn biguint_to_u256_known_value() {
405 let n = BigUint::from(0x1234_5678u64);
406 let result = biguint_to_u256(&n);
407 assert_eq!(result, alloy::primitives::U256::from(0x1234_5678u64));
408 }
409
410 #[test]
415 fn transaction_from_dto() {
416 use tycho_simulation::tycho_core::Bytes as TychoBytes;
417 let router_bytes = vec![0x01u8; 20];
418 let dto_tx = dto::Transaction::new(
419 TychoBytes::from(Bytes::copy_from_slice(&router_bytes)),
420 BigUint::ZERO,
421 vec![0x12, 0x34],
422 );
423 let tx = Transaction::from(dto_tx);
424 assert_eq!(tx.to().as_ref(), router_bytes.as_slice());
425 assert_eq!(tx.value(), &BigUint::ZERO);
426 assert_eq!(tx.data(), &[0x12, 0x34]);
427 }
428
429 #[test]
434 fn bytes_to_alloy_address_happy_path() {
435 let b = Bytes::copy_from_slice(&[0xab; 20]);
436 let addr = bytes_to_alloy_address(&b).unwrap();
437 assert_eq!(addr.as_slice(), &[0xab; 20]);
438 }
439
440 #[test]
441 fn bytes_to_alloy_address_wrong_length() {
442 let b = Bytes::copy_from_slice(&[0xab; 4]);
443 assert!(matches!(bytes_to_alloy_address(&b), Err(FyndError::Protocol(_))));
444 }
445
446 #[test]
451 fn swap_try_from_dto_happy_path() {
452 let client_swap = Swap::try_from(sample_dto_swap()).unwrap();
453 assert_eq!(client_swap.component_id(), "pool-1");
454 assert_eq!(client_swap.protocol(), "uniswap-v3");
455 assert_eq!(client_swap.token_in(), &Bytes::copy_from_slice(&[0xaa; 20]));
456 assert_eq!(client_swap.token_out(), &Bytes::copy_from_slice(&[0xbb; 20]));
457 assert_eq!(client_swap.amount_in(), &BigUint::from(100u32));
458 assert_eq!(client_swap.amount_out(), &BigUint::from(99u32));
459 assert_eq!(client_swap.gas_estimate(), &BigUint::from(50_000u32));
460 }
461
462 #[test]
467 fn quote_status_all_variants() {
468 use dto::QuoteStatus as Dto;
469 assert!(matches!(QuoteStatus::from(Dto::Success), QuoteStatus::Success));
470 assert!(matches!(QuoteStatus::from(Dto::NoRouteFound), QuoteStatus::NoRouteFound));
471 assert!(matches!(
472 QuoteStatus::from(Dto::InsufficientLiquidity),
473 QuoteStatus::InsufficientLiquidity
474 ));
475 assert!(matches!(QuoteStatus::from(Dto::Timeout), QuoteStatus::Timeout));
476 assert!(matches!(QuoteStatus::from(Dto::NotReady), QuoteStatus::NotReady));
477 }
478
479 #[test]
484 fn block_info_from_dto() {
485 let dto_block = sample_dto_block();
486 let block = BlockInfo::from(dto_block);
487 assert_eq!(block.number(), 21_000_000);
488 assert_eq!(block.hash(), "0xdeadbeef");
489 assert_eq!(block.timestamp(), 1_730_000_000);
490 }
491
492 #[test]
497 fn quote_from_dto() {
498 let ds = sample_dto_order_quote();
499 let quote = dto_to_quote(ds, Bytes::new(), Bytes::new()).unwrap();
500 assert_eq!(quote.order_id(), "test-order-id");
501 assert!(matches!(quote.status(), QuoteStatus::Success));
502 assert!(matches!(quote.backend(), BackendKind::Fynd));
503 assert_eq!(quote.amount_in(), &BigUint::from(1_000u32));
504 assert_eq!(quote.amount_out(), &BigUint::from(999u32));
505 assert_eq!(quote.gas_estimate(), &BigUint::from(100_000u32));
506 assert_eq!(quote.amount_out_net_gas(), &BigUint::from(998u32));
507 assert_eq!(quote.price_impact_bps(), Some(5));
508 assert!(quote.token_out().is_empty());
510 assert!(quote.receiver().is_empty());
511 }
512
513 #[test]
518 fn order_try_from_client_encodes_addresses_as_tycho() {
519 let order = Order::new(
520 Bytes::copy_from_slice(&[0xaa; 20]),
521 Bytes::copy_from_slice(&[0xbb; 20]),
522 BigUint::from(1_000u32),
523 OrderSide::Sell,
524 Bytes::copy_from_slice(&[0xcc; 20]),
525 None,
526 );
527
528 let dto_order = dto::Order::try_from(order).unwrap();
529 assert_eq!(dto_order.token_in().0, Bytes::copy_from_slice(&[0xaa; 20]));
531 assert_eq!(dto_order.token_out().0, Bytes::copy_from_slice(&[0xbb; 20]));
532 assert_eq!(dto_order.sender().0, Bytes::copy_from_slice(&[0xcc; 20]));
533 assert!(dto_order.receiver().is_none());
534 assert_eq!(dto_order.amount(), &BigUint::from(1_000u32));
535 }
536
537 #[test]
538 fn order_try_from_client_with_receiver() {
539 let order = Order::new(
540 Bytes::copy_from_slice(&[0xaa; 20]),
541 Bytes::copy_from_slice(&[0xbb; 20]),
542 BigUint::from(1u32),
543 OrderSide::Sell,
544 Bytes::copy_from_slice(&[0xcc; 20]),
545 Some(Bytes::copy_from_slice(&[0xdd; 20])),
546 );
547
548 let dto_order = dto::Order::try_from(order).unwrap();
549 let receiver = dto_order.receiver().unwrap();
550 assert_eq!(receiver.0, Bytes::copy_from_slice(&[0xdd; 20]));
551 }
552
553 #[test]
554 fn order_try_from_client_invalid_address_length() {
555 let order = Order::new(
556 Bytes::copy_from_slice(&[0xaa; 4]), Bytes::copy_from_slice(&[0xbb; 20]),
558 BigUint::from(1u32),
559 OrderSide::Sell,
560 Bytes::copy_from_slice(&[0xcc; 20]),
561 None,
562 );
563 assert!(matches!(dto::Order::try_from(order), Err(FyndError::Protocol(_))));
564 }
565
566 #[test]
571 fn user_transfer_type_permit2_maps_correctly() {
572 let result = dto::UserTransferType::from(UserTransferType::TransferFromPermit2);
573 assert!(matches!(result, dto::UserTransferType::TransferFromPermit2));
574 }
575
576 #[test]
577 fn user_transfer_type_vault_funds_maps_correctly() {
578 let result = dto::UserTransferType::from(UserTransferType::UseVaultsFunds);
579 assert!(matches!(result, dto::UserTransferType::UseVaultsFunds));
580 }
581
582 #[test]
587 fn permit_details_try_from_happy_path() {
588 let details = PermitDetails::new(
589 Bytes::copy_from_slice(&[0xaa; 20]),
590 BigUint::from(1_000u32),
591 BigUint::from(9_999_999u32),
592 BigUint::from(0u32),
593 );
594 let dto_details = dto::PermitDetails::try_from(details).unwrap();
595 assert_eq!(dto_details.token().as_ref(), &[0xaa; 20]);
596 assert_eq!(dto_details.amount(), &BigUint::from(1_000u32));
597 assert_eq!(dto_details.expiration(), &BigUint::from(9_999_999u32));
598 assert_eq!(dto_details.nonce(), &BigUint::from(0u32));
599 }
600
601 #[test]
602 fn permit_details_try_from_invalid_token() {
603 let details = PermitDetails::new(
604 Bytes::copy_from_slice(&[0xaa; 4]), BigUint::from(1u32),
606 BigUint::from(1u32),
607 BigUint::from(0u32),
608 );
609 assert!(matches!(dto::PermitDetails::try_from(details), Err(FyndError::Protocol(_))));
610 }
611
612 #[test]
617 fn permit_single_try_from_happy_path() {
618 let details = PermitDetails::new(
619 Bytes::copy_from_slice(&[0xaa; 20]),
620 BigUint::from(500u32),
621 BigUint::from(1_000_000u32),
622 BigUint::from(1u32),
623 );
624 let permit = PermitSingle::new(
625 details,
626 Bytes::copy_from_slice(&[0xbb; 20]),
627 BigUint::from(2_000_000u32),
628 );
629 let dto_permit = dto::PermitSingle::try_from(permit).unwrap();
630 assert_eq!(dto_permit.spender().as_ref(), &[0xbb; 20]);
631 assert_eq!(dto_permit.sig_deadline(), &BigUint::from(2_000_000u32));
632 assert_eq!(dto_permit.details().amount(), &BigUint::from(500u32));
633 }
634
635 #[test]
640 fn encoding_options_try_from_with_permit2() {
641 use crate::types::{EncodingOptions, PermitDetails, PermitSingle};
642
643 let details = PermitDetails::new(
644 Bytes::copy_from_slice(&[0xaa; 20]),
645 BigUint::from(1_000u32),
646 BigUint::from(9_999_999u32),
647 BigUint::from(0u32),
648 );
649 let permit = PermitSingle::new(
650 details,
651 Bytes::copy_from_slice(&[0xbb; 20]),
652 BigUint::from(9_999_999u32),
653 );
654 let sig = Bytes::copy_from_slice(&[0xcc; 65]);
655 let opts = EncodingOptions::new(0.005)
656 .with_permit2(permit, sig.clone())
657 .unwrap();
658
659 let dto_opts = dto::EncodingOptions::try_from(opts).unwrap();
660 assert!(matches!(dto_opts.transfer_type(), dto::UserTransferType::TransferFromPermit2));
661 assert!(dto_opts.permit().is_some());
662 assert_eq!(
663 dto_opts
664 .permit2_signature()
665 .unwrap()
666 .as_ref(),
667 sig.as_ref()
668 );
669 }
670
671 #[test]
676 fn encoding_options_try_from_with_client_fee() {
677 use crate::types::{ClientFeeParams, EncodingOptions};
678
679 let fee = ClientFeeParams::new(
680 100,
681 Bytes::copy_from_slice(&[0x44; 20]),
682 BigUint::from(500_000u64),
683 1_893_456_000u64,
684 )
685 .with_signature(Bytes::copy_from_slice(&[0xAB; 65]));
686 let opts = EncodingOptions::new(0.01).with_client_fee(fee);
687
688 let dto_opts = dto::EncodingOptions::try_from(opts).unwrap();
689 assert!(dto_opts.client_fee_params().is_some());
690 let dto_fee = dto_opts.client_fee_params().unwrap();
691 assert_eq!(dto_fee.bps(), 100);
692 assert_eq!(*dto_fee.max_contribution(), BigUint::from(500_000u64));
693 assert_eq!(dto_fee.deadline(), 1_893_456_000u64);
694 assert_eq!(dto_fee.signature().len(), 65);
695 }
696
697 #[test]
698 fn encoding_options_try_from_without_client_fee() {
699 use crate::types::EncodingOptions;
700
701 let opts = EncodingOptions::new(0.005);
702 let dto_opts = dto::EncodingOptions::try_from(opts).unwrap();
703 assert!(dto_opts.client_fee_params().is_none());
704 }
705}