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, 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 Ok(dto_opts)
141 }
142}
143
144impl TryFrom<EncodingOptions> for dto::EncodingOptions {
145 type Error = FyndError;
146
147 fn try_from(opts: EncodingOptions) -> Result<Self, Self::Error> {
148 let mut dto_opts =
149 dto::EncodingOptions::new(opts.slippage).with_transfer_type(opts.transfer_type.into());
150 if let (Some(permit), Some(sig)) = (
151 opts.permit
152 .map(dto::PermitSingle::try_from)
153 .transpose()?,
154 opts.permit2_signature
155 .map(|b| dto::Bytes::from(b.as_ref())),
156 ) {
157 dto_opts = dto_opts.with_permit2(permit, sig);
158 }
159 if let Some(fee) = opts.client_fee_params {
160 dto_opts = dto_opts.with_client_fee_params(dto::ClientFeeParams::new(
161 fee.bps,
162 dto::Bytes::from(fee.receiver.as_ref()),
163 fee.max_contribution,
164 fee.deadline,
165 dto::Bytes::from(
166 fee.signature
167 .unwrap_or_default()
168 .as_ref(),
169 ),
170 ));
171 }
172 Ok(dto_opts)
173 }
174}
175
176impl TryFrom<PermitSingle> for dto::PermitSingle {
177 type Error = FyndError;
178
179 fn try_from(p: PermitSingle) -> Result<Self, Self::Error> {
180 let details = dto::PermitDetails::try_from(p.details)?;
181 let spender = bytes_to_dto_addr(&p.spender)?;
182 Ok(dto::PermitSingle::new(details, spender, p.sig_deadline))
183 }
184}
185
186impl TryFrom<PermitDetails> for dto::PermitDetails {
187 type Error = FyndError;
188
189 fn try_from(d: PermitDetails) -> Result<Self, Self::Error> {
190 let token = bytes_to_dto_addr(&d.token)?;
191 Ok(dto::PermitDetails::new(token, d.amount, d.expiration, d.nonce))
192 }
193}
194
195impl From<UserTransferType> for dto::UserTransferType {
196 fn from(t: UserTransferType) -> Self {
197 match t {
198 UserTransferType::TransferFrom => dto::UserTransferType::TransferFrom,
199 UserTransferType::TransferFromPermit2 => dto::UserTransferType::TransferFromPermit2,
200 UserTransferType::UseVaultsFunds => dto::UserTransferType::UseVaultsFunds,
201 }
202 }
203}
204
205fn order_quote_to_quote(
210 order_quote: OrderQuote,
211 token_out: bytes::Bytes,
212 receiver: bytes::Bytes,
213) -> Result<Quote, FyndError> {
214 let status = QuoteStatus::from(order_quote.status());
215 let route = order_quote
216 .route()
217 .cloned()
218 .map(Route::try_from)
219 .transpose()?;
220 let block = BlockInfo::from(order_quote.block().clone());
221 let transaction = order_quote
222 .transaction()
223 .cloned()
224 .map(Transaction::from);
225 let fee_breakdown = order_quote.fee_breakdown().map(|fb| {
226 FeeBreakdown::new(
227 fb.router_fee().clone(),
228 fb.client_fee().clone(),
229 fb.max_slippage().clone(),
230 fb.min_amount_received().clone(),
231 )
232 });
233 Ok(Quote::new(
234 order_quote.order_id().to_string(),
235 status,
236 BackendKind::Fynd,
237 route,
238 order_quote.amount_in().clone(),
239 order_quote.amount_out().clone(),
240 order_quote.gas_estimate().clone(),
241 order_quote.amount_out_net_gas().clone(),
242 order_quote.price_impact_bps(),
243 block,
244 token_out,
245 receiver,
246 transaction,
247 fee_breakdown,
248 ))
249}
250
251impl From<dto::Transaction> for Transaction {
252 fn from(dt: dto::Transaction) -> Self {
253 Transaction::new(
254 bytes::Bytes::copy_from_slice(dt.to().as_ref()),
255 dt.value().clone(),
256 dt.data().to_vec(),
257 )
258 }
259}
260
261pub(crate) fn map_quote_response(
267 response: dto::Quote,
268 order_meta: Vec<(bytes::Bytes, bytes::Bytes)>,
269) -> Result<Vec<Quote>, FyndError> {
270 let solve_time_ms = response.solve_time_ms();
271 let order_quotes = response.into_orders();
272 if order_quotes.len() != order_meta.len() {
273 return Err(FyndError::Protocol(format!(
274 "server returned {} quotes but {} were requested",
275 order_quotes.len(),
276 order_meta.len()
277 )));
278 }
279 order_quotes
280 .into_iter()
281 .zip(order_meta)
282 .map(|(oq, (token_out, receiver))| {
283 let mut quote = order_quote_to_quote(oq, token_out, receiver)?;
284 quote.solve_time_ms = solve_time_ms;
285 Ok(quote)
286 })
287 .collect()
288}
289
290impl From<dto::QuoteStatus> for QuoteStatus {
291 fn from(ds: dto::QuoteStatus) -> Self {
292 match ds {
293 dto::QuoteStatus::Success => Self::Success,
294 dto::QuoteStatus::NoRouteFound => Self::NoRouteFound,
295 dto::QuoteStatus::InsufficientLiquidity => Self::InsufficientLiquidity,
296 dto::QuoteStatus::Timeout => Self::Timeout,
297 dto::QuoteStatus::NotReady => Self::NotReady,
298 _ => unreachable!("unexpected QuoteStatus variant"),
299 }
300 }
301}
302
303impl TryFrom<dto::Route> for Route {
304 type Error = FyndError;
305
306 fn try_from(dr: dto::Route) -> Result<Self, Self::Error> {
307 let swaps = dr
308 .into_swaps()
309 .into_iter()
310 .map(Swap::try_from)
311 .collect::<Result<Vec<_>, _>>()?;
312 Ok(Route::new(swaps))
313 }
314}
315
316impl TryFrom<dto::Swap> for Swap {
317 type Error = FyndError;
318
319 fn try_from(ds: dto::Swap) -> Result<Self, Self::Error> {
320 let token_in = dto_addr_to_bytes(ds.token_in().clone());
321 let token_out = dto_addr_to_bytes(ds.token_out().clone());
322 Ok(Swap::new(
323 ds.component_id().to_string(),
324 ds.protocol().to_string(),
325 token_in,
326 token_out,
327 ds.amount_in().clone(),
328 ds.amount_out().clone(),
329 ds.gas_estimate().clone(),
330 ds.split(),
331 ))
332 }
333}
334
335impl From<dto::BlockInfo> for BlockInfo {
336 fn from(db: dto::BlockInfo) -> Self {
337 BlockInfo::new(db.number(), db.hash().to_string(), db.timestamp())
338 }
339}
340
341impl TryFrom<fynd_rpc_types::InstanceInfo> for crate::types::InstanceInfo {
342 type Error = FyndError;
343
344 fn try_from(dto: fynd_rpc_types::InstanceInfo) -> Result<Self, Self::Error> {
345 let router = bytes::Bytes::copy_from_slice(dto.router_address().as_ref());
346 let permit2 = bytes::Bytes::copy_from_slice(dto.permit2_address().as_ref());
347 if router.len() != 20 {
348 return Err(FyndError::Protocol(format!(
349 "router_address must be 20 bytes, got {}",
350 router.len()
351 )));
352 }
353 if permit2.len() != 20 {
354 return Err(FyndError::Protocol(format!(
355 "permit2_address must be 20 bytes, got {}",
356 permit2.len()
357 )));
358 }
359 Ok(crate::types::InstanceInfo::new(router, permit2, dto.chain_id()))
360 }
361}
362
363impl From<dto::HealthStatus> for HealthStatus {
364 fn from(dh: dto::HealthStatus) -> Self {
365 HealthStatus::new(
366 dh.healthy(),
367 dh.last_update_ms(),
368 dh.num_solver_pools(),
369 dh.derived_data_ready(),
370 dh.gas_price_age_ms(),
371 )
372 }
373}
374
375pub(crate) fn dto_error_to_fynd(de: dto::ErrorResponse) -> FyndError {
380 let code = ErrorCode::from_server_code(de.code());
381 FyndError::Api { code, message: de.error().to_string() }
382}
383
384#[cfg(test)]
385mod tests {
386 use bytes::Bytes;
387 use num_bigint::BigUint;
388
389 use super::*;
390
391 fn sample_dto_swap() -> dto::Swap {
392 serde_json::from_str(
393 r#"{
394 "component_id": "pool-1",
395 "protocol": "uniswap-v3",
396 "token_in": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
397 "token_out": "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
398 "amount_in": "100",
399 "amount_out": "99",
400 "gas_estimate": "50000",
401 "split": "0"
402 }"#,
403 )
404 .expect("valid swap JSON")
405 }
406
407 fn sample_dto_block() -> dto::BlockInfo {
408 serde_json::from_str(
409 r#"{
410 "number": 21000000,
411 "hash": "0xdeadbeef",
412 "timestamp": 1730000000
413 }"#,
414 )
415 .expect("valid block JSON")
416 }
417
418 fn sample_dto_order_quote() -> dto::OrderQuote {
419 serde_json::from_str(
420 r#"{
421 "order_id": "test-order-id",
422 "status": "success",
423 "amount_in": "1000",
424 "amount_out": "999",
425 "gas_estimate": "100000",
426 "price_impact_bps": 5,
427 "amount_out_net_gas": "998",
428 "block": {"number": 21000000, "hash": "0xdeadbeef", "timestamp": 1730000000}
429 }"#,
430 )
431 .expect("valid order quote JSON")
432 }
433
434 #[test]
439 fn biguint_to_u256_zero() {
440 let result = biguint_to_u256(&BigUint::ZERO);
441 assert_eq!(result, alloy::primitives::U256::ZERO);
442 }
443
444 #[test]
445 fn biguint_to_u256_known_value() {
446 let n = BigUint::from(0x1234_5678u64);
447 let result = biguint_to_u256(&n);
448 assert_eq!(result, alloy::primitives::U256::from(0x1234_5678u64));
449 }
450
451 #[test]
456 fn transaction_from_dto() {
457 let router_bytes = vec![0x01u8; 20];
458 let dto_tx = dto::Transaction::new(
459 dto::Bytes::from(router_bytes.as_slice()),
460 BigUint::ZERO,
461 vec![0x12, 0x34],
462 );
463 let tx = Transaction::from(dto_tx);
464 assert_eq!(tx.to().as_ref(), router_bytes.as_slice());
465 assert_eq!(tx.value(), &BigUint::ZERO);
466 assert_eq!(tx.data(), &[0x12, 0x34]);
467 }
468
469 #[test]
474 fn bytes_to_alloy_address_happy_path() {
475 let b = Bytes::copy_from_slice(&[0xab; 20]);
476 let addr = bytes_to_alloy_address(&b).unwrap();
477 assert_eq!(addr.as_slice(), &[0xab; 20]);
478 }
479
480 #[test]
481 fn bytes_to_alloy_address_wrong_length() {
482 let b = Bytes::copy_from_slice(&[0xab; 4]);
483 assert!(matches!(bytes_to_alloy_address(&b), Err(FyndError::Protocol(_))));
484 }
485
486 #[test]
491 fn swap_try_from_dto_happy_path() {
492 let client_swap = Swap::try_from(sample_dto_swap()).unwrap();
493 assert_eq!(client_swap.component_id(), "pool-1");
494 assert_eq!(client_swap.protocol(), "uniswap-v3");
495 assert_eq!(client_swap.token_in(), &Bytes::copy_from_slice(&[0xaa; 20]));
496 assert_eq!(client_swap.token_out(), &Bytes::copy_from_slice(&[0xbb; 20]));
497 assert_eq!(client_swap.amount_in(), &BigUint::from(100u32));
498 assert_eq!(client_swap.amount_out(), &BigUint::from(99u32));
499 assert_eq!(client_swap.gas_estimate(), &BigUint::from(50_000u32));
500 }
501
502 #[test]
507 fn quote_status_all_variants() {
508 use dto::QuoteStatus as Dto;
509 assert!(matches!(QuoteStatus::from(Dto::Success), QuoteStatus::Success));
510 assert!(matches!(QuoteStatus::from(Dto::NoRouteFound), QuoteStatus::NoRouteFound));
511 assert!(matches!(
512 QuoteStatus::from(Dto::InsufficientLiquidity),
513 QuoteStatus::InsufficientLiquidity
514 ));
515 assert!(matches!(QuoteStatus::from(Dto::Timeout), QuoteStatus::Timeout));
516 assert!(matches!(QuoteStatus::from(Dto::NotReady), QuoteStatus::NotReady));
517 }
518
519 #[test]
524 fn block_info_from_dto() {
525 let dto_block = sample_dto_block();
526 let block = BlockInfo::from(dto_block);
527 assert_eq!(block.number(), 21_000_000);
528 assert_eq!(block.hash(), "0xdeadbeef");
529 assert_eq!(block.timestamp(), 1_730_000_000);
530 }
531
532 #[test]
537 fn quote_from_dto() {
538 let ds = sample_dto_order_quote();
539 let quote = order_quote_to_quote(ds, Bytes::new(), Bytes::new()).unwrap();
540 assert_eq!(quote.order_id(), "test-order-id");
541 assert!(matches!(quote.status(), QuoteStatus::Success));
542 assert!(matches!(quote.backend(), BackendKind::Fynd));
543 assert_eq!(quote.amount_in(), &BigUint::from(1_000u32));
544 assert_eq!(quote.amount_out(), &BigUint::from(999u32));
545 assert_eq!(quote.gas_estimate(), &BigUint::from(100_000u32));
546 assert_eq!(quote.amount_out_net_gas(), &BigUint::from(998u32));
547 assert_eq!(quote.price_impact_bps(), Some(5));
548 assert!(quote.token_out().is_empty());
550 assert!(quote.receiver().is_empty());
551 }
552
553 #[test]
558 fn order_try_from_client_encodes_addresses_as_tycho() {
559 let order = Order::new(
560 Bytes::copy_from_slice(&[0xaa; 20]),
561 Bytes::copy_from_slice(&[0xbb; 20]),
562 BigUint::from(1_000u32),
563 OrderSide::Sell,
564 Bytes::copy_from_slice(&[0xcc; 20]),
565 None,
566 );
567
568 let dto_order = dto::Order::try_from(order).unwrap();
569 assert_eq!(dto_order.token_in().as_ref(), &[0xaa; 20]);
570 assert_eq!(dto_order.token_out().as_ref(), &[0xbb; 20]);
571 assert_eq!(dto_order.sender().as_ref(), &[0xcc; 20]);
572 assert!(dto_order.receiver().is_none());
573 assert_eq!(dto_order.amount(), &BigUint::from(1_000u32));
574 }
575
576 #[test]
577 fn order_try_from_client_with_receiver() {
578 let order = Order::new(
579 Bytes::copy_from_slice(&[0xaa; 20]),
580 Bytes::copy_from_slice(&[0xbb; 20]),
581 BigUint::from(1u32),
582 OrderSide::Sell,
583 Bytes::copy_from_slice(&[0xcc; 20]),
584 Some(Bytes::copy_from_slice(&[0xdd; 20])),
585 );
586
587 let dto_order = dto::Order::try_from(order).unwrap();
588 let receiver = dto_order.receiver().unwrap();
589 assert_eq!(receiver.as_ref(), &[0xdd; 20]);
590 }
591
592 #[test]
593 fn order_try_from_client_invalid_address_length() {
594 let order = Order::new(
595 Bytes::copy_from_slice(&[0xaa; 4]), Bytes::copy_from_slice(&[0xbb; 20]),
597 BigUint::from(1u32),
598 OrderSide::Sell,
599 Bytes::copy_from_slice(&[0xcc; 20]),
600 None,
601 );
602 assert!(matches!(dto::Order::try_from(order), Err(FyndError::Protocol(_))));
603 }
604
605 #[test]
610 fn user_transfer_type_permit2_maps_correctly() {
611 let result = dto::UserTransferType::from(UserTransferType::TransferFromPermit2);
612 assert!(matches!(result, dto::UserTransferType::TransferFromPermit2));
613 }
614
615 #[test]
616 fn user_transfer_type_vault_funds_maps_correctly() {
617 let result = dto::UserTransferType::from(UserTransferType::UseVaultsFunds);
618 assert!(matches!(result, dto::UserTransferType::UseVaultsFunds));
619 }
620
621 #[test]
626 fn permit_details_try_from_happy_path() {
627 let details = PermitDetails::new(
628 Bytes::copy_from_slice(&[0xaa; 20]),
629 BigUint::from(1_000u32),
630 BigUint::from(9_999_999u32),
631 BigUint::from(0u32),
632 );
633 let dto_details = dto::PermitDetails::try_from(details).unwrap();
634 assert_eq!(dto_details.token().as_ref(), &[0xaa; 20]);
635 assert_eq!(dto_details.amount(), &BigUint::from(1_000u32));
636 assert_eq!(dto_details.expiration(), &BigUint::from(9_999_999u32));
637 assert_eq!(dto_details.nonce(), &BigUint::from(0u32));
638 }
639
640 #[test]
641 fn permit_details_try_from_invalid_token() {
642 let details = PermitDetails::new(
643 Bytes::copy_from_slice(&[0xaa; 4]), BigUint::from(1u32),
645 BigUint::from(1u32),
646 BigUint::from(0u32),
647 );
648 assert!(matches!(dto::PermitDetails::try_from(details), Err(FyndError::Protocol(_))));
649 }
650
651 #[test]
656 fn permit_single_try_from_happy_path() {
657 let details = PermitDetails::new(
658 Bytes::copy_from_slice(&[0xaa; 20]),
659 BigUint::from(500u32),
660 BigUint::from(1_000_000u32),
661 BigUint::from(1u32),
662 );
663 let permit = PermitSingle::new(
664 details,
665 Bytes::copy_from_slice(&[0xbb; 20]),
666 BigUint::from(2_000_000u32),
667 );
668 let dto_permit = dto::PermitSingle::try_from(permit).unwrap();
669 assert_eq!(dto_permit.spender().as_ref(), &[0xbb; 20]);
670 assert_eq!(dto_permit.sig_deadline(), &BigUint::from(2_000_000u32));
671 assert_eq!(dto_permit.details().amount(), &BigUint::from(500u32));
672 }
673
674 #[test]
679 fn encoding_options_try_from_with_permit2() {
680 use crate::types::{EncodingOptions, PermitDetails, PermitSingle};
681
682 let details = PermitDetails::new(
683 Bytes::copy_from_slice(&[0xaa; 20]),
684 BigUint::from(1_000u32),
685 BigUint::from(9_999_999u32),
686 BigUint::from(0u32),
687 );
688 let permit = PermitSingle::new(
689 details,
690 Bytes::copy_from_slice(&[0xbb; 20]),
691 BigUint::from(9_999_999u32),
692 );
693 let sig = Bytes::copy_from_slice(&[0xcc; 65]);
694 let opts = EncodingOptions::new(0.005)
695 .with_permit2(permit, sig.clone())
696 .unwrap();
697
698 let dto_opts = dto::EncodingOptions::try_from(opts).unwrap();
699 assert!(matches!(dto_opts.transfer_type(), dto::UserTransferType::TransferFromPermit2));
700 assert!(dto_opts.permit().is_some());
701 assert_eq!(
702 dto_opts
703 .permit2_signature()
704 .unwrap()
705 .as_ref(),
706 sig.as_ref()
707 );
708 }
709
710 #[test]
715 fn encoding_options_try_from_with_client_fee() {
716 use crate::types::{ClientFeeParams, EncodingOptions};
717
718 let fee = ClientFeeParams::new(
719 100,
720 Bytes::copy_from_slice(&[0x44; 20]),
721 BigUint::from(500_000u64),
722 1_893_456_000u64,
723 )
724 .with_signature(Bytes::copy_from_slice(&[0xAB; 65]));
725 let opts = EncodingOptions::new(0.01).with_client_fee(fee);
726
727 let dto_opts = dto::EncodingOptions::try_from(opts).unwrap();
728 assert!(dto_opts.client_fee_params().is_some());
729 let dto_fee = dto_opts.client_fee_params().unwrap();
730 assert_eq!(dto_fee.bps(), 100);
731 assert_eq!(*dto_fee.max_contribution(), BigUint::from(500_000u64));
732 assert_eq!(dto_fee.deadline(), 1_893_456_000u64);
733 assert_eq!(dto_fee.signature().len(), 65);
734 }
735
736 #[test]
737 fn encoding_options_try_from_without_client_fee() {
738 use crate::types::EncodingOptions;
739
740 let opts = EncodingOptions::new(0.005);
741 let dto_opts = dto::EncodingOptions::try_from(opts).unwrap();
742 assert!(dto_opts.client_fee_params().is_none());
743 }
744
745 #[test]
750 fn batch_quote_params_to_dto_empty_orders_errors() {
751 use crate::types::{BatchQuoteParams, QuoteOptions};
752
753 let params = BatchQuoteParams::new(vec![], QuoteOptions::default());
754 assert!(matches!(batch_quote_params_to_dto(params), Err(FyndError::Protocol(_))));
755 }
756
757 #[test]
758 fn batch_quote_params_to_dto_extracts_per_order_meta() {
759 use crate::types::{BatchQuoteParams, Order, OrderSide, QuoteOptions};
760
761 let token_in_a = Bytes::copy_from_slice(&[0xaa; 20]);
762 let token_out_a = Bytes::copy_from_slice(&[0xbb; 20]);
763 let sender_a = Bytes::copy_from_slice(&[0xcc; 20]);
764 let receiver_a = Bytes::copy_from_slice(&[0xdd; 20]);
765
766 let token_in_b = Bytes::copy_from_slice(&[0x11; 20]);
767 let token_out_b = Bytes::copy_from_slice(&[0x22; 20]);
768 let sender_b = Bytes::copy_from_slice(&[0x33; 20]);
769
770 let order_a = Order::new(
771 token_in_a,
772 token_out_a.clone(),
773 BigUint::from(1_000u32),
774 OrderSide::Sell,
775 sender_a.clone(),
776 Some(receiver_a.clone()),
777 );
778 let order_b = Order::new(
779 token_in_b,
780 token_out_b.clone(),
781 BigUint::from(2_000u32),
782 OrderSide::Sell,
783 sender_b.clone(),
784 None, );
786
787 let params = BatchQuoteParams::new(vec![order_a, order_b], QuoteOptions::default());
788 let (request, meta) = batch_quote_params_to_dto(params).unwrap();
789
790 assert_eq!(request.orders().len(), 2);
791 assert_eq!(meta.len(), 2);
792
793 let (tok_out_0, rec_0) = &meta[0];
794 assert_eq!(tok_out_0.as_ref(), token_out_a.as_ref());
795 assert_eq!(rec_0.as_ref(), receiver_a.as_ref());
796
797 let (tok_out_1, rec_1) = &meta[1];
798 assert_eq!(tok_out_1.as_ref(), token_out_b.as_ref());
799 assert_eq!(rec_1.as_ref(), sender_b.as_ref());
801 }
802
803 #[test]
808 fn map_quote_response_count_mismatch_errors() {
809 let oq = sample_dto_order_quote();
810 let dto_quote = dto::Quote::new(vec![oq], BigUint::from(100_000u32), 42);
811 let meta = vec![(Bytes::new(), Bytes::new()), (Bytes::new(), Bytes::new())];
813 assert!(matches!(map_quote_response(dto_quote, meta), Err(FyndError::Protocol(_))));
814 }
815
816 #[test]
817 fn map_quote_response_maps_per_order_meta_and_solve_time() {
818 let oq_a = sample_dto_order_quote();
819 let oq_b = sample_dto_order_quote();
820 let solve_ms = 77u64;
821 let dto_quote = dto::Quote::new(vec![oq_a, oq_b], BigUint::from(200_000u32), solve_ms);
822
823 let token_out_a = Bytes::copy_from_slice(&[0xaa; 20]);
824 let receiver_a = Bytes::copy_from_slice(&[0xbb; 20]);
825 let token_out_b = Bytes::copy_from_slice(&[0xcc; 20]);
826 let receiver_b = Bytes::copy_from_slice(&[0xdd; 20]);
827
828 let meta = vec![
829 (token_out_a.clone(), receiver_a.clone()),
830 (token_out_b.clone(), receiver_b.clone()),
831 ];
832
833 let quotes = map_quote_response(dto_quote, meta).unwrap();
834 assert_eq!(quotes.len(), 2);
835
836 assert_eq!(quotes[0].token_out().as_ref(), token_out_a.as_ref());
837 assert_eq!(quotes[0].receiver().as_ref(), receiver_a.as_ref());
838 assert_eq!(quotes[0].solve_time_ms(), solve_ms);
839
840 assert_eq!(quotes[1].token_out().as_ref(), token_out_b.as_ref());
841 assert_eq!(quotes[1].receiver().as_ref(), receiver_b.as_ref());
842 assert_eq!(quotes[1].solve_time_ms(), solve_ms);
843 }
844}