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