1use fynd_rpc_types as dto;
7use fynd_rpc_types::OrderQuote;
8
9use crate::{
10 error::{ErrorCode, FyndError},
11 types::{
12 BackendKind, BatchQuoteParams, BlockInfo, EncodingOptions, FeeBreakdown, HealthStatus,
13 Order, OrderSide, PermitDetails, PermitSingle, Quote, QuoteOptions, QuoteParams,
14 QuoteStatus, Route, SimulationResult, 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
63pub(crate) fn batch_quote_params_to_dto(
69 params: BatchQuoteParams,
70) -> Result<(dto::QuoteRequest, Vec<(bytes::Bytes, bytes::Bytes)>), FyndError> {
71 if params.orders.is_empty() {
72 return Err(FyndError::Protocol("batch_quote requires at least one order".into()));
73 }
74 let options = dto::QuoteOptions::try_from(params.options)?;
75 let mut dto_orders = Vec::with_capacity(params.orders.len());
76 let mut order_meta = Vec::with_capacity(params.orders.len());
77 for order in params.orders {
78 let token_out = order.token_out().clone();
79 let receiver = order
80 .receiver()
81 .unwrap_or_else(|| order.sender())
82 .clone();
83 dto_orders.push(dto::Order::try_from(order)?);
84 order_meta.push((token_out, receiver));
85 }
86 let request = dto::QuoteRequest::new(dto_orders).with_options(options);
87 Ok((request, order_meta))
88}
89
90impl TryFrom<Order> for dto::Order {
91 type Error = FyndError;
92
93 fn try_from(order: Order) -> Result<Self, Self::Error> {
94 let token_in = bytes_to_dto_addr(order.token_in())?;
95 let token_out = bytes_to_dto_addr(order.token_out())?;
96 let sender = bytes_to_dto_addr(order.sender())?;
97 let receiver = order
98 .receiver()
99 .map(bytes_to_dto_addr)
100 .transpose()?;
101 let mut dto_order = dto::Order::new(
102 token_in,
103 token_out,
104 order.amount().clone(),
105 order.side().into(),
106 sender,
107 );
108 if let Some(r) = receiver {
109 dto_order = dto_order.with_receiver(r);
110 }
111 Ok(dto_order)
112 }
113}
114
115impl From<OrderSide> for dto::OrderSide {
116 fn from(side: OrderSide) -> Self {
117 match side {
118 OrderSide::Sell => dto::OrderSide::Sell,
119 }
120 }
121}
122
123impl TryFrom<QuoteOptions> for dto::QuoteOptions {
124 type Error = FyndError;
125
126 fn try_from(opts: QuoteOptions) -> Result<Self, Self::Error> {
127 let mut dto_opts = dto::QuoteOptions::default();
128 if let Some(ms) = opts.timeout_ms {
129 dto_opts = dto_opts.with_timeout_ms(ms);
130 }
131 if let Some(n) = opts.min_responses {
132 dto_opts = dto_opts.with_min_responses(n);
133 }
134 if let Some(gas) = opts.max_gas {
135 dto_opts = dto_opts.with_max_gas(gas);
136 }
137 if let Some(enc) = opts.encoding_options {
138 dto_opts = dto_opts.with_encoding_options(dto::EncodingOptions::try_from(enc)?);
139 }
140 if let Some(filter) = opts.route_filter {
141 dto_opts = dto_opts.with_route_filter(filter);
142 }
143 Ok(dto_opts)
144 }
145}
146
147impl TryFrom<EncodingOptions> for dto::EncodingOptions {
148 type Error = FyndError;
149
150 fn try_from(opts: EncodingOptions) -> Result<Self, Self::Error> {
151 let mut dto_opts =
152 dto::EncodingOptions::new(opts.slippage).with_transfer_type(opts.transfer_type.into());
153 if let (Some(permit), Some(sig)) = (
154 opts.permit
155 .map(dto::PermitSingle::try_from)
156 .transpose()?,
157 opts.permit2_signature
158 .map(|b| dto::Bytes::from(b.as_ref())),
159 ) {
160 dto_opts = dto_opts.with_permit2(permit, sig);
161 }
162 if let Some(fee) = opts.client_fee_params {
163 dto_opts = dto_opts.with_client_fee_params(dto::ClientFeeParams::new(
164 fee.bps,
165 dto::Bytes::from(fee.receiver.as_ref()),
166 fee.max_contribution,
167 fee.deadline,
168 dto::Bytes::default(),
171 ));
172 }
173 if let Some(pg) = opts.price_guard {
174 dto_opts = dto_opts.with_price_guard(pg);
175 }
176 if opts.simulate {
177 dto_opts = dto_opts.with_simulation();
178 }
179 Ok(dto_opts)
180 }
181}
182
183impl TryFrom<PermitSingle> for dto::PermitSingle {
184 type Error = FyndError;
185
186 fn try_from(p: PermitSingle) -> Result<Self, Self::Error> {
187 let details = dto::PermitDetails::try_from(p.details)?;
188 let spender = bytes_to_dto_addr(&p.spender)?;
189 Ok(dto::PermitSingle::new(details, spender, p.sig_deadline))
190 }
191}
192
193impl TryFrom<PermitDetails> for dto::PermitDetails {
194 type Error = FyndError;
195
196 fn try_from(d: PermitDetails) -> Result<Self, Self::Error> {
197 let token = bytes_to_dto_addr(&d.token)?;
198 Ok(dto::PermitDetails::new(token, d.amount, d.expiration, d.nonce))
199 }
200}
201
202impl From<UserTransferType> for dto::UserTransferType {
203 fn from(t: UserTransferType) -> Self {
204 match t {
205 UserTransferType::TransferFrom => dto::UserTransferType::TransferFrom,
206 UserTransferType::TransferFromPermit2 => dto::UserTransferType::TransferFromPermit2,
207 UserTransferType::UseVaultsFunds => dto::UserTransferType::UseVaultsFunds,
208 }
209 }
210}
211
212fn order_quote_to_quote(
217 order_quote: OrderQuote,
218 token_out: bytes::Bytes,
219 receiver: bytes::Bytes,
220) -> Result<Quote, FyndError> {
221 let status = QuoteStatus::from(order_quote.status());
222 let route = order_quote
223 .route()
224 .cloned()
225 .map(Route::try_from)
226 .transpose()?;
227 let block = BlockInfo::from(order_quote.block().clone());
228 let transaction = order_quote
229 .transaction()
230 .cloned()
231 .map(Transaction::from);
232 let fee_breakdown = order_quote.fee_breakdown().map(|fb| {
233 let swaps_hash = fb.swaps_hash().and_then(|b| {
234 let arr: [u8; 32] = b.0.as_ref().try_into().ok()?;
235 Some(arr)
236 });
237 FeeBreakdown::new(
238 fb.router_fee().clone(),
239 fb.client_fee().clone(),
240 fb.max_slippage().clone(),
241 fb.min_amount_received().clone(),
242 swaps_hash,
243 )
244 });
245 let mut quote = Quote::new(
246 order_quote.order_id().to_string(),
247 status,
248 BackendKind::Fynd,
249 route,
250 order_quote.amount_in().clone(),
251 order_quote.amount_out().clone(),
252 order_quote.gas_estimate().clone(),
253 order_quote.amount_out_net_gas().clone(),
254 order_quote.price_impact_bps(),
255 block,
256 token_out,
257 receiver,
258 transaction,
259 fee_breakdown,
260 );
261 quote.algorithm = order_quote
262 .algorithm()
263 .map(str::to_string);
264 quote.simulation_result = order_quote
265 .simulation_result()
266 .cloned()
267 .map(|result| match result {
268 dto::SimulationResult::Success { amount_out, gas_used } => {
269 SimulationResult::Success { amount_out, gas_used }
270 }
271 dto::SimulationResult::Failure { reason } => SimulationResult::Failure { reason },
272 });
273 Ok(quote)
274}
275
276impl From<dto::Transaction> for Transaction {
277 fn from(dt: dto::Transaction) -> Self {
278 let mut tx = Transaction::new(
279 bytes::Bytes::copy_from_slice(dt.to().as_ref()),
280 dt.value().clone(),
281 dt.data().to_vec(),
282 );
283 if let Some(offset) = dt.client_fee_signature_offset() {
284 tx.client_fee_signature_offset = Some(offset);
285 }
286 tx
287 }
288}
289
290pub(crate) fn map_quote_response(
296 response: dto::Quote,
297 order_meta: Vec<(bytes::Bytes, bytes::Bytes)>,
298) -> Result<Vec<Quote>, FyndError> {
299 let solve_time_ms = response.solve_time_ms();
300 let order_quotes = response.into_orders();
301 if order_quotes.len() != order_meta.len() {
302 return Err(FyndError::Protocol(format!(
303 "server returned {} quotes but {} were requested",
304 order_quotes.len(),
305 order_meta.len()
306 )));
307 }
308 order_quotes
309 .into_iter()
310 .zip(order_meta)
311 .map(|(oq, (token_out, receiver))| {
312 let mut quote = order_quote_to_quote(oq, token_out, receiver)?;
313 quote.solve_time_ms = solve_time_ms;
314 Ok(quote)
315 })
316 .collect()
317}
318
319impl From<dto::QuoteStatus> for QuoteStatus {
320 fn from(ds: dto::QuoteStatus) -> Self {
321 match ds {
322 dto::QuoteStatus::Success => Self::Success,
323 dto::QuoteStatus::NoRouteFound => Self::NoRouteFound,
324 dto::QuoteStatus::InsufficientLiquidity => Self::InsufficientLiquidity,
325 dto::QuoteStatus::Timeout => Self::Timeout,
326 dto::QuoteStatus::NotReady => Self::NotReady,
327 dto::QuoteStatus::PriceCheckFailed => Self::PriceCheckFailed,
328 dto::QuoteStatus::EncodingFailed => Self::EncodingFailed,
329 _ => Self::NotReady,
330 }
331 }
332}
333
334impl TryFrom<dto::Route> for Route {
335 type Error = FyndError;
336
337 fn try_from(dr: dto::Route) -> Result<Self, Self::Error> {
338 let swaps = dr
339 .into_swaps()
340 .into_iter()
341 .map(Swap::try_from)
342 .collect::<Result<Vec<_>, _>>()?;
343 Ok(Route::new(swaps))
344 }
345}
346
347impl TryFrom<dto::Swap> for Swap {
348 type Error = FyndError;
349
350 fn try_from(ds: dto::Swap) -> Result<Self, Self::Error> {
351 let token_in = dto_addr_to_bytes(ds.token_in().clone());
352 let token_out = dto_addr_to_bytes(ds.token_out().clone());
353 Ok(Swap::new(
354 ds.component_id().to_string(),
355 ds.protocol().to_string(),
356 token_in,
357 token_out,
358 ds.amount_in().clone(),
359 ds.amount_out().clone(),
360 ds.gas_estimate().clone(),
361 ds.split(),
362 ))
363 }
364}
365
366impl From<dto::BlockInfo> for BlockInfo {
367 fn from(db: dto::BlockInfo) -> Self {
368 BlockInfo::new(db.number(), db.hash().to_string(), db.timestamp())
369 }
370}
371
372impl TryFrom<fynd_rpc_types::InstanceInfo> for crate::types::InstanceInfo {
373 type Error = FyndError;
374
375 fn try_from(dto: fynd_rpc_types::InstanceInfo) -> Result<Self, Self::Error> {
376 let router = dto
377 .router_address()
378 .map(|r| bytes::Bytes::copy_from_slice(r.as_ref()));
379 let permit2 = bytes::Bytes::copy_from_slice(dto.permit2_address().as_ref());
380 if let Some(router) = &router {
381 if router.len() != 20 {
382 return Err(FyndError::Protocol(format!(
383 "router_address must be 20 bytes, got {}",
384 router.len()
385 )));
386 }
387 }
388 if permit2.len() != 20 {
389 return Err(FyndError::Protocol(format!(
390 "permit2_address must be 20 bytes, got {}",
391 permit2.len()
392 )));
393 }
394 Ok(crate::types::InstanceInfo::new(
395 router,
396 permit2,
397 dto.chain_id(),
398 dto.version().to_string(),
399 ))
400 }
401}
402
403impl From<dto::HealthStatus> for HealthStatus {
404 fn from(dh: dto::HealthStatus) -> Self {
405 HealthStatus::new(
406 dh.healthy(),
407 dh.last_update_ms(),
408 dh.num_solver_pools(),
409 dh.derived_data_ready(),
410 dh.gas_price_age_ms(),
411 )
412 }
413}
414
415pub(crate) fn dto_error_to_fynd(de: dto::ErrorResponse) -> FyndError {
420 let code = ErrorCode::from_server_code(de.code());
421 FyndError::Api { code, message: de.error().to_string() }
422}
423
424#[cfg(test)]
425mod tests {
426 use bytes::Bytes;
427 use num_bigint::BigUint;
428
429 use super::*;
430
431 fn sample_dto_swap() -> dto::Swap {
432 serde_json::from_str(
433 r#"{
434 "component_id": "component-1",
435 "protocol": "uniswap-v3",
436 "token_in": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
437 "token_out": "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
438 "amount_in": "100",
439 "amount_out": "99",
440 "gas_estimate": "50000",
441 "split": "0"
442 }"#,
443 )
444 .expect("valid swap JSON")
445 }
446
447 fn sample_dto_block() -> dto::BlockInfo {
448 serde_json::from_str(
449 r#"{
450 "number": 21000000,
451 "hash": "0xdeadbeef",
452 "timestamp": 1730000000
453 }"#,
454 )
455 .expect("valid block JSON")
456 }
457
458 fn sample_dto_order_quote() -> dto::OrderQuote {
459 serde_json::from_str(
460 r#"{
461 "order_id": "test-order-id",
462 "status": "success",
463 "amount_in": "1000",
464 "amount_out": "999",
465 "gas_estimate": "100000",
466 "price_impact_bps": 5,
467 "amount_out_net_gas": "998",
468 "block": {"number": 21000000, "hash": "0xdeadbeef", "timestamp": 1730000000}
469 }"#,
470 )
471 .expect("valid order quote JSON")
472 }
473
474 #[test]
479 fn instance_info_maps_version() {
480 let dto: fynd_rpc_types::InstanceInfo = serde_json::from_str(
481 r#"{
482 "version": "9.9.9",
483 "chain_id": 1,
484 "router_address": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
485 "permit2_address": "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
486 }"#,
487 )
488 .expect("valid instance info JSON");
489
490 let mapped: crate::types::InstanceInfo = dto.try_into().expect("maps");
491 assert_eq!(mapped.version(), "9.9.9");
492 assert_eq!(mapped.chain_id(), 1);
493 }
494
495 #[test]
500 fn biguint_to_u256_zero() {
501 let result = biguint_to_u256(&BigUint::ZERO);
502 assert_eq!(result, alloy::primitives::U256::ZERO);
503 }
504
505 #[test]
506 fn biguint_to_u256_known_value() {
507 let n = BigUint::from(0x1234_5678u64);
508 let result = biguint_to_u256(&n);
509 assert_eq!(result, alloy::primitives::U256::from(0x1234_5678u64));
510 }
511
512 #[test]
517 fn transaction_from_dto() {
518 let router_bytes = vec![0x01u8; 20];
519 let dto_tx = dto::Transaction::new(
520 dto::Bytes::from(router_bytes.as_slice()),
521 BigUint::ZERO,
522 vec![0x12, 0x34],
523 );
524 let tx = Transaction::from(dto_tx);
525 assert_eq!(tx.to().as_ref(), router_bytes.as_slice());
526 assert_eq!(tx.value(), &BigUint::ZERO);
527 assert_eq!(tx.data(), &[0x12, 0x34]);
528 }
529
530 #[test]
535 fn bytes_to_alloy_address_happy_path() {
536 let b = Bytes::copy_from_slice(&[0xab; 20]);
537 let addr = bytes_to_alloy_address(&b).unwrap();
538 assert_eq!(addr.as_slice(), &[0xab; 20]);
539 }
540
541 #[test]
542 fn bytes_to_alloy_address_wrong_length() {
543 let b = Bytes::copy_from_slice(&[0xab; 4]);
544 assert!(matches!(bytes_to_alloy_address(&b), Err(FyndError::Protocol(_))));
545 }
546
547 #[test]
552 fn swap_try_from_dto_happy_path() {
553 let client_swap = Swap::try_from(sample_dto_swap()).unwrap();
554 assert_eq!(client_swap.component_id(), "component-1");
555 assert_eq!(client_swap.protocol(), "uniswap-v3");
556 assert_eq!(client_swap.token_in(), &Bytes::copy_from_slice(&[0xaa; 20]));
557 assert_eq!(client_swap.token_out(), &Bytes::copy_from_slice(&[0xbb; 20]));
558 assert_eq!(client_swap.amount_in(), &BigUint::from(100u32));
559 assert_eq!(client_swap.amount_out(), &BigUint::from(99u32));
560 assert_eq!(client_swap.gas_estimate(), &BigUint::from(50_000u32));
561 }
562
563 #[test]
568 fn quote_status_all_variants() {
569 use dto::QuoteStatus as Dto;
570 assert!(matches!(QuoteStatus::from(Dto::Success), QuoteStatus::Success));
571 assert!(matches!(QuoteStatus::from(Dto::NoRouteFound), QuoteStatus::NoRouteFound));
572 assert!(matches!(
573 QuoteStatus::from(Dto::InsufficientLiquidity),
574 QuoteStatus::InsufficientLiquidity
575 ));
576 assert!(matches!(QuoteStatus::from(Dto::Timeout), QuoteStatus::Timeout));
577 assert!(matches!(QuoteStatus::from(Dto::NotReady), QuoteStatus::NotReady));
578 assert!(matches!(QuoteStatus::from(Dto::PriceCheckFailed), QuoteStatus::PriceCheckFailed));
579 assert!(matches!(QuoteStatus::from(Dto::EncodingFailed), QuoteStatus::EncodingFailed));
580 }
581
582 #[test]
587 fn block_info_from_dto() {
588 let dto_block = sample_dto_block();
589 let block = BlockInfo::from(dto_block);
590 assert_eq!(block.number(), 21_000_000);
591 assert_eq!(block.hash(), "0xdeadbeef");
592 assert_eq!(block.timestamp(), 1_730_000_000);
593 }
594
595 #[test]
600 fn quote_from_dto() {
601 let ds = sample_dto_order_quote();
602 let quote = order_quote_to_quote(ds, Bytes::new(), Bytes::new()).unwrap();
603 assert_eq!(quote.order_id(), "test-order-id");
604 assert!(matches!(quote.status(), QuoteStatus::Success));
605 assert!(matches!(quote.backend(), BackendKind::Fynd));
606 assert_eq!(quote.amount_in(), &BigUint::from(1_000u32));
607 assert_eq!(quote.amount_out(), &BigUint::from(999u32));
608 assert_eq!(quote.gas_estimate(), &BigUint::from(100_000u32));
609 assert_eq!(quote.amount_out_net_gas(), &BigUint::from(998u32));
610 assert_eq!(quote.price_impact_bps(), Some(5));
611 assert!(quote.token_out().is_empty());
613 assert!(quote.receiver().is_empty());
614 assert_eq!(quote.algorithm(), None);
616 }
617
618 #[test]
619 fn quote_from_dto_algorithm() {
620 let ds: dto::OrderQuote = serde_json::from_str(
621 r#"{
622 "order_id": "test-order-id",
623 "status": "success",
624 "amount_in": "1000",
625 "amount_out": "999",
626 "gas_estimate": "100000",
627 "amount_out_net_gas": "998",
628 "algorithm": "bellman_ford",
629 "block": {"number": 21000000, "hash": "0xdeadbeef", "timestamp": 1730000000}
630 }"#,
631 )
632 .expect("valid order quote JSON");
633
634 let quote = order_quote_to_quote(ds, Bytes::new(), Bytes::new()).unwrap();
635 assert_eq!(quote.algorithm(), Some("bellman_ford"));
636 }
637
638 #[test]
643 fn order_try_from_client_encodes_addresses_as_tycho() {
644 let order = Order::new(
645 Bytes::copy_from_slice(&[0xaa; 20]),
646 Bytes::copy_from_slice(&[0xbb; 20]),
647 BigUint::from(1_000u32),
648 OrderSide::Sell,
649 Bytes::copy_from_slice(&[0xcc; 20]),
650 None,
651 );
652
653 let dto_order = dto::Order::try_from(order).unwrap();
654 assert_eq!(dto_order.token_in().as_ref(), &[0xaa; 20]);
655 assert_eq!(dto_order.token_out().as_ref(), &[0xbb; 20]);
656 assert_eq!(dto_order.sender().as_ref(), &[0xcc; 20]);
657 assert!(dto_order.receiver().is_none());
658 assert_eq!(dto_order.amount(), &BigUint::from(1_000u32));
659 }
660
661 #[test]
662 fn order_try_from_client_with_receiver() {
663 let order = Order::new(
664 Bytes::copy_from_slice(&[0xaa; 20]),
665 Bytes::copy_from_slice(&[0xbb; 20]),
666 BigUint::from(1u32),
667 OrderSide::Sell,
668 Bytes::copy_from_slice(&[0xcc; 20]),
669 Some(Bytes::copy_from_slice(&[0xdd; 20])),
670 );
671
672 let dto_order = dto::Order::try_from(order).unwrap();
673 let receiver = dto_order.receiver().unwrap();
674 assert_eq!(receiver.as_ref(), &[0xdd; 20]);
675 }
676
677 #[test]
678 fn order_try_from_client_invalid_address_length() {
679 let order = Order::new(
680 Bytes::copy_from_slice(&[0xaa; 4]), Bytes::copy_from_slice(&[0xbb; 20]),
682 BigUint::from(1u32),
683 OrderSide::Sell,
684 Bytes::copy_from_slice(&[0xcc; 20]),
685 None,
686 );
687 assert!(matches!(dto::Order::try_from(order), Err(FyndError::Protocol(_))));
688 }
689
690 #[test]
695 fn user_transfer_type_permit2_maps_correctly() {
696 let result = dto::UserTransferType::from(UserTransferType::TransferFromPermit2);
697 assert!(matches!(result, dto::UserTransferType::TransferFromPermit2));
698 }
699
700 #[test]
701 fn user_transfer_type_vault_funds_maps_correctly() {
702 let result = dto::UserTransferType::from(UserTransferType::UseVaultsFunds);
703 assert!(matches!(result, dto::UserTransferType::UseVaultsFunds));
704 }
705
706 #[test]
711 fn permit_details_try_from_happy_path() {
712 let details = PermitDetails::new(
713 Bytes::copy_from_slice(&[0xaa; 20]),
714 BigUint::from(1_000u32),
715 BigUint::from(9_999_999u32),
716 BigUint::from(0u32),
717 );
718 let dto_details = dto::PermitDetails::try_from(details).unwrap();
719 assert_eq!(dto_details.token().as_ref(), &[0xaa; 20]);
720 assert_eq!(dto_details.amount(), &BigUint::from(1_000u32));
721 assert_eq!(dto_details.expiration(), &BigUint::from(9_999_999u32));
722 assert_eq!(dto_details.nonce(), &BigUint::from(0u32));
723 }
724
725 #[test]
726 fn permit_details_try_from_invalid_token() {
727 let details = PermitDetails::new(
728 Bytes::copy_from_slice(&[0xaa; 4]), BigUint::from(1u32),
730 BigUint::from(1u32),
731 BigUint::from(0u32),
732 );
733 assert!(matches!(dto::PermitDetails::try_from(details), Err(FyndError::Protocol(_))));
734 }
735
736 #[test]
741 fn permit_single_try_from_happy_path() {
742 let details = PermitDetails::new(
743 Bytes::copy_from_slice(&[0xaa; 20]),
744 BigUint::from(500u32),
745 BigUint::from(1_000_000u32),
746 BigUint::from(1u32),
747 );
748 let permit = PermitSingle::new(
749 details,
750 Bytes::copy_from_slice(&[0xbb; 20]),
751 BigUint::from(2_000_000u32),
752 );
753 let dto_permit = dto::PermitSingle::try_from(permit).unwrap();
754 assert_eq!(dto_permit.spender().as_ref(), &[0xbb; 20]);
755 assert_eq!(dto_permit.sig_deadline(), &BigUint::from(2_000_000u32));
756 assert_eq!(dto_permit.details().amount(), &BigUint::from(500u32));
757 }
758
759 #[test]
764 fn encoding_options_try_from_with_permit2() {
765 use crate::types::{EncodingOptions, PermitDetails, PermitSingle};
766
767 let details = PermitDetails::new(
768 Bytes::copy_from_slice(&[0xaa; 20]),
769 BigUint::from(1_000u32),
770 BigUint::from(9_999_999u32),
771 BigUint::from(0u32),
772 );
773 let permit = PermitSingle::new(
774 details,
775 Bytes::copy_from_slice(&[0xbb; 20]),
776 BigUint::from(9_999_999u32),
777 );
778 let sig = Bytes::copy_from_slice(&[0xcc; 65]);
779 let opts = EncodingOptions::new(0.005)
780 .with_permit2(permit, sig.clone())
781 .unwrap();
782
783 let dto_opts = dto::EncodingOptions::try_from(opts).unwrap();
784 assert!(matches!(dto_opts.transfer_type(), dto::UserTransferType::TransferFromPermit2));
785 assert!(dto_opts.permit().is_some());
786 assert_eq!(
787 dto_opts
788 .permit2_signature()
789 .unwrap()
790 .as_ref(),
791 sig.as_ref()
792 );
793 }
794
795 #[test]
800 fn encoding_options_try_from_with_client_fee() {
801 use crate::types::{ClientFeeParams, EncodingOptions};
802
803 let fee = ClientFeeParams::new(
804 100,
805 Bytes::copy_from_slice(&[0x44; 20]),
806 BigUint::from(500_000u64),
807 1_893_456_000u64,
808 );
809 let opts = EncodingOptions::new(0.01).with_client_fee(fee);
810
811 let dto_opts = dto::EncodingOptions::try_from(opts).unwrap();
812 assert!(dto_opts.client_fee_params().is_some());
813 let dto_fee = dto_opts.client_fee_params().unwrap();
814 assert_eq!(dto_fee.bps(), 100);
815 assert_eq!(*dto_fee.max_contribution(), BigUint::from(500_000u64));
816 assert_eq!(dto_fee.deadline(), 1_893_456_000u64);
817 assert!(dto_fee.signature().is_empty());
819 }
820
821 #[test]
822 fn encoding_options_try_from_without_client_fee() {
823 use crate::types::EncodingOptions;
824
825 let opts = EncodingOptions::new(0.005);
826 let dto_opts = dto::EncodingOptions::try_from(opts).unwrap();
827 assert!(dto_opts.client_fee_params().is_none());
828 }
829
830 #[test]
831 fn test_quote_options_to_dto_route_filter() {
832 use crate::types::{QuoteOptions, RouteFilter};
833
834 let opts = QuoteOptions::default().with_route_filter(
835 RouteFilter::default()
836 .with_excluded_pools(["pool-1".to_string()])
837 .with_excluded_protocols(["uniswap_v2".to_string()]),
838 );
839
840 let dto_opts = dto::QuoteOptions::try_from(opts).unwrap();
841 let filter = dto_opts.route_filter().unwrap();
842
843 assert_eq!(filter.excluded_pools(), ["pool-1".to_string()]);
844 assert_eq!(filter.excluded_protocols(), ["uniswap_v2".to_string()]);
845 }
846
847 #[test]
852 fn batch_quote_params_to_dto_empty_orders_errors() {
853 use crate::types::{BatchQuoteParams, QuoteOptions};
854
855 let params = BatchQuoteParams::new(vec![], QuoteOptions::default());
856 assert!(matches!(batch_quote_params_to_dto(params), Err(FyndError::Protocol(_))));
857 }
858
859 #[test]
860 fn batch_quote_params_to_dto_extracts_per_order_meta() {
861 use crate::types::{BatchQuoteParams, Order, OrderSide, QuoteOptions};
862
863 let token_in_a = Bytes::copy_from_slice(&[0xaa; 20]);
864 let token_out_a = Bytes::copy_from_slice(&[0xbb; 20]);
865 let sender_a = Bytes::copy_from_slice(&[0xcc; 20]);
866 let receiver_a = Bytes::copy_from_slice(&[0xdd; 20]);
867
868 let token_in_b = Bytes::copy_from_slice(&[0x11; 20]);
869 let token_out_b = Bytes::copy_from_slice(&[0x22; 20]);
870 let sender_b = Bytes::copy_from_slice(&[0x33; 20]);
871
872 let order_a = Order::new(
873 token_in_a,
874 token_out_a.clone(),
875 BigUint::from(1_000u32),
876 OrderSide::Sell,
877 sender_a.clone(),
878 Some(receiver_a.clone()),
879 );
880 let order_b = Order::new(
881 token_in_b,
882 token_out_b.clone(),
883 BigUint::from(2_000u32),
884 OrderSide::Sell,
885 sender_b.clone(),
886 None, );
888
889 let params = BatchQuoteParams::new(vec![order_a, order_b], QuoteOptions::default());
890 let (request, meta) = batch_quote_params_to_dto(params).unwrap();
891
892 assert_eq!(request.orders().len(), 2);
893 assert_eq!(meta.len(), 2);
894
895 let (tok_out_0, rec_0) = &meta[0];
896 assert_eq!(tok_out_0.as_ref(), token_out_a.as_ref());
897 assert_eq!(rec_0.as_ref(), receiver_a.as_ref());
898
899 let (tok_out_1, rec_1) = &meta[1];
900 assert_eq!(tok_out_1.as_ref(), token_out_b.as_ref());
901 assert_eq!(rec_1.as_ref(), sender_b.as_ref());
903 }
904
905 #[test]
910 fn map_quote_response_count_mismatch_errors() {
911 let oq = sample_dto_order_quote();
912 let dto_quote = dto::Quote::new(vec![oq], BigUint::from(100_000u32), 42);
913 let meta = vec![(Bytes::new(), Bytes::new()), (Bytes::new(), Bytes::new())];
915 assert!(matches!(map_quote_response(dto_quote, meta), Err(FyndError::Protocol(_))));
916 }
917
918 #[test]
919 fn map_quote_response_maps_per_order_meta_and_solve_time() {
920 let oq_a = sample_dto_order_quote();
921 let oq_b = sample_dto_order_quote();
922 let solve_ms = 77u64;
923 let dto_quote = dto::Quote::new(vec![oq_a, oq_b], BigUint::from(200_000u32), solve_ms);
924
925 let token_out_a = Bytes::copy_from_slice(&[0xaa; 20]);
926 let receiver_a = Bytes::copy_from_slice(&[0xbb; 20]);
927 let token_out_b = Bytes::copy_from_slice(&[0xcc; 20]);
928 let receiver_b = Bytes::copy_from_slice(&[0xdd; 20]);
929
930 let meta = vec![
931 (token_out_a.clone(), receiver_a.clone()),
932 (token_out_b.clone(), receiver_b.clone()),
933 ];
934
935 let quotes = map_quote_response(dto_quote, meta).unwrap();
936 assert_eq!(quotes.len(), 2);
937
938 assert_eq!(quotes[0].token_out().as_ref(), token_out_a.as_ref());
939 assert_eq!(quotes[0].receiver().as_ref(), receiver_a.as_ref());
940 assert_eq!(quotes[0].solve_time_ms(), solve_ms);
941
942 assert_eq!(quotes[1].token_out().as_ref(), token_out_b.as_ref());
943 assert_eq!(quotes[1].receiver().as_ref(), receiver_b.as_ref());
944 assert_eq!(quotes[1].solve_time_ms(), solve_ms);
945 }
946}