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 if let Some(pg) = opts.price_guard {
173 dto_opts = dto_opts.with_price_guard(pg);
174 }
175 Ok(dto_opts)
176 }
177}
178
179impl TryFrom<PermitSingle> for dto::PermitSingle {
180 type Error = FyndError;
181
182 fn try_from(p: PermitSingle) -> Result<Self, Self::Error> {
183 let details = dto::PermitDetails::try_from(p.details)?;
184 let spender = bytes_to_dto_addr(&p.spender)?;
185 Ok(dto::PermitSingle::new(details, spender, p.sig_deadline))
186 }
187}
188
189impl TryFrom<PermitDetails> for dto::PermitDetails {
190 type Error = FyndError;
191
192 fn try_from(d: PermitDetails) -> Result<Self, Self::Error> {
193 let token = bytes_to_dto_addr(&d.token)?;
194 Ok(dto::PermitDetails::new(token, d.amount, d.expiration, d.nonce))
195 }
196}
197
198impl From<UserTransferType> for dto::UserTransferType {
199 fn from(t: UserTransferType) -> Self {
200 match t {
201 UserTransferType::TransferFrom => dto::UserTransferType::TransferFrom,
202 UserTransferType::TransferFromPermit2 => dto::UserTransferType::TransferFromPermit2,
203 UserTransferType::UseVaultsFunds => dto::UserTransferType::UseVaultsFunds,
204 }
205 }
206}
207
208fn order_quote_to_quote(
213 order_quote: OrderQuote,
214 token_out: bytes::Bytes,
215 receiver: bytes::Bytes,
216) -> Result<Quote, FyndError> {
217 let status = QuoteStatus::from(order_quote.status());
218 let route = order_quote
219 .route()
220 .cloned()
221 .map(Route::try_from)
222 .transpose()?;
223 let block = BlockInfo::from(order_quote.block().clone());
224 let transaction = order_quote
225 .transaction()
226 .cloned()
227 .map(Transaction::from);
228 let fee_breakdown = order_quote.fee_breakdown().map(|fb| {
229 let swaps_hash = fb.swaps_hash().and_then(|b| {
230 let arr: [u8; 32] = b.0.as_ref().try_into().ok()?;
231 Some(arr)
232 });
233 FeeBreakdown::new(
234 fb.router_fee().clone(),
235 fb.client_fee().clone(),
236 fb.max_slippage().clone(),
237 fb.min_amount_received().clone(),
238 swaps_hash,
239 )
240 });
241 Ok(Quote::new(
242 order_quote.order_id().to_string(),
243 status,
244 BackendKind::Fynd,
245 route,
246 order_quote.amount_in().clone(),
247 order_quote.amount_out().clone(),
248 order_quote.gas_estimate().clone(),
249 order_quote.amount_out_net_gas().clone(),
250 order_quote.price_impact_bps(),
251 block,
252 token_out,
253 receiver,
254 transaction,
255 fee_breakdown,
256 ))
257}
258
259impl From<dto::Transaction> for Transaction {
260 fn from(dt: dto::Transaction) -> Self {
261 let mut tx = Transaction::new(
262 bytes::Bytes::copy_from_slice(dt.to().as_ref()),
263 dt.value().clone(),
264 dt.data().to_vec(),
265 );
266 if let Some(offset) = dt.client_fee_signature_offset() {
267 tx.client_fee_signature_offset = Some(offset);
268 }
269 tx
270 }
271}
272
273pub(crate) fn map_quote_response(
279 response: dto::Quote,
280 order_meta: Vec<(bytes::Bytes, bytes::Bytes)>,
281) -> Result<Vec<Quote>, FyndError> {
282 let solve_time_ms = response.solve_time_ms();
283 let order_quotes = response.into_orders();
284 if order_quotes.len() != order_meta.len() {
285 return Err(FyndError::Protocol(format!(
286 "server returned {} quotes but {} were requested",
287 order_quotes.len(),
288 order_meta.len()
289 )));
290 }
291 order_quotes
292 .into_iter()
293 .zip(order_meta)
294 .map(|(oq, (token_out, receiver))| {
295 let mut quote = order_quote_to_quote(oq, token_out, receiver)?;
296 quote.solve_time_ms = solve_time_ms;
297 Ok(quote)
298 })
299 .collect()
300}
301
302impl From<dto::QuoteStatus> for QuoteStatus {
303 fn from(ds: dto::QuoteStatus) -> Self {
304 match ds {
305 dto::QuoteStatus::Success => Self::Success,
306 dto::QuoteStatus::NoRouteFound => Self::NoRouteFound,
307 dto::QuoteStatus::InsufficientLiquidity => Self::InsufficientLiquidity,
308 dto::QuoteStatus::Timeout => Self::Timeout,
309 dto::QuoteStatus::NotReady => Self::NotReady,
310 dto::QuoteStatus::PriceCheckFailed => Self::PriceCheckFailed,
311 _ => Self::NotReady,
312 }
313 }
314}
315
316impl TryFrom<dto::Route> for Route {
317 type Error = FyndError;
318
319 fn try_from(dr: dto::Route) -> Result<Self, Self::Error> {
320 let swaps = dr
321 .into_swaps()
322 .into_iter()
323 .map(Swap::try_from)
324 .collect::<Result<Vec<_>, _>>()?;
325 Ok(Route::new(swaps))
326 }
327}
328
329impl TryFrom<dto::Swap> for Swap {
330 type Error = FyndError;
331
332 fn try_from(ds: dto::Swap) -> Result<Self, Self::Error> {
333 let token_in = dto_addr_to_bytes(ds.token_in().clone());
334 let token_out = dto_addr_to_bytes(ds.token_out().clone());
335 Ok(Swap::new(
336 ds.component_id().to_string(),
337 ds.protocol().to_string(),
338 token_in,
339 token_out,
340 ds.amount_in().clone(),
341 ds.amount_out().clone(),
342 ds.gas_estimate().clone(),
343 ds.split(),
344 ))
345 }
346}
347
348impl From<dto::BlockInfo> for BlockInfo {
349 fn from(db: dto::BlockInfo) -> Self {
350 BlockInfo::new(db.number(), db.hash().to_string(), db.timestamp())
351 }
352}
353
354impl TryFrom<fynd_rpc_types::InstanceInfo> for crate::types::InstanceInfo {
355 type Error = FyndError;
356
357 fn try_from(dto: fynd_rpc_types::InstanceInfo) -> Result<Self, Self::Error> {
358 let router = dto
359 .router_address()
360 .map(|r| bytes::Bytes::copy_from_slice(r.as_ref()));
361 let permit2 = bytes::Bytes::copy_from_slice(dto.permit2_address().as_ref());
362 if let Some(router) = &router {
363 if router.len() != 20 {
364 return Err(FyndError::Protocol(format!(
365 "router_address must be 20 bytes, got {}",
366 router.len()
367 )));
368 }
369 }
370 if permit2.len() != 20 {
371 return Err(FyndError::Protocol(format!(
372 "permit2_address must be 20 bytes, got {}",
373 permit2.len()
374 )));
375 }
376 Ok(crate::types::InstanceInfo::new(
377 router,
378 permit2,
379 dto.chain_id(),
380 dto.version().to_string(),
381 ))
382 }
383}
384
385impl From<dto::HealthStatus> for HealthStatus {
386 fn from(dh: dto::HealthStatus) -> Self {
387 HealthStatus::new(
388 dh.healthy(),
389 dh.last_update_ms(),
390 dh.num_solver_pools(),
391 dh.derived_data_ready(),
392 dh.gas_price_age_ms(),
393 )
394 }
395}
396
397pub(crate) fn dto_error_to_fynd(de: dto::ErrorResponse) -> FyndError {
402 let code = ErrorCode::from_server_code(de.code());
403 FyndError::Api { code, message: de.error().to_string() }
404}
405
406#[cfg(test)]
407mod tests {
408 use bytes::Bytes;
409 use num_bigint::BigUint;
410
411 use super::*;
412
413 fn sample_dto_swap() -> dto::Swap {
414 serde_json::from_str(
415 r#"{
416 "component_id": "pool-1",
417 "protocol": "uniswap-v3",
418 "token_in": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
419 "token_out": "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
420 "amount_in": "100",
421 "amount_out": "99",
422 "gas_estimate": "50000",
423 "split": "0"
424 }"#,
425 )
426 .expect("valid swap JSON")
427 }
428
429 fn sample_dto_block() -> dto::BlockInfo {
430 serde_json::from_str(
431 r#"{
432 "number": 21000000,
433 "hash": "0xdeadbeef",
434 "timestamp": 1730000000
435 }"#,
436 )
437 .expect("valid block JSON")
438 }
439
440 fn sample_dto_order_quote() -> dto::OrderQuote {
441 serde_json::from_str(
442 r#"{
443 "order_id": "test-order-id",
444 "status": "success",
445 "amount_in": "1000",
446 "amount_out": "999",
447 "gas_estimate": "100000",
448 "price_impact_bps": 5,
449 "amount_out_net_gas": "998",
450 "block": {"number": 21000000, "hash": "0xdeadbeef", "timestamp": 1730000000}
451 }"#,
452 )
453 .expect("valid order quote JSON")
454 }
455
456 #[test]
461 fn instance_info_maps_version() {
462 let dto: fynd_rpc_types::InstanceInfo = serde_json::from_str(
463 r#"{
464 "version": "9.9.9",
465 "chain_id": 1,
466 "router_address": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
467 "permit2_address": "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
468 }"#,
469 )
470 .expect("valid instance info JSON");
471
472 let mapped: crate::types::InstanceInfo = dto.try_into().expect("maps");
473 assert_eq!(mapped.version(), "9.9.9");
474 assert_eq!(mapped.chain_id(), 1);
475 }
476
477 #[test]
482 fn biguint_to_u256_zero() {
483 let result = biguint_to_u256(&BigUint::ZERO);
484 assert_eq!(result, alloy::primitives::U256::ZERO);
485 }
486
487 #[test]
488 fn biguint_to_u256_known_value() {
489 let n = BigUint::from(0x1234_5678u64);
490 let result = biguint_to_u256(&n);
491 assert_eq!(result, alloy::primitives::U256::from(0x1234_5678u64));
492 }
493
494 #[test]
499 fn transaction_from_dto() {
500 let router_bytes = vec![0x01u8; 20];
501 let dto_tx = dto::Transaction::new(
502 dto::Bytes::from(router_bytes.as_slice()),
503 BigUint::ZERO,
504 vec![0x12, 0x34],
505 );
506 let tx = Transaction::from(dto_tx);
507 assert_eq!(tx.to().as_ref(), router_bytes.as_slice());
508 assert_eq!(tx.value(), &BigUint::ZERO);
509 assert_eq!(tx.data(), &[0x12, 0x34]);
510 }
511
512 #[test]
517 fn bytes_to_alloy_address_happy_path() {
518 let b = Bytes::copy_from_slice(&[0xab; 20]);
519 let addr = bytes_to_alloy_address(&b).unwrap();
520 assert_eq!(addr.as_slice(), &[0xab; 20]);
521 }
522
523 #[test]
524 fn bytes_to_alloy_address_wrong_length() {
525 let b = Bytes::copy_from_slice(&[0xab; 4]);
526 assert!(matches!(bytes_to_alloy_address(&b), Err(FyndError::Protocol(_))));
527 }
528
529 #[test]
534 fn swap_try_from_dto_happy_path() {
535 let client_swap = Swap::try_from(sample_dto_swap()).unwrap();
536 assert_eq!(client_swap.component_id(), "pool-1");
537 assert_eq!(client_swap.protocol(), "uniswap-v3");
538 assert_eq!(client_swap.token_in(), &Bytes::copy_from_slice(&[0xaa; 20]));
539 assert_eq!(client_swap.token_out(), &Bytes::copy_from_slice(&[0xbb; 20]));
540 assert_eq!(client_swap.amount_in(), &BigUint::from(100u32));
541 assert_eq!(client_swap.amount_out(), &BigUint::from(99u32));
542 assert_eq!(client_swap.gas_estimate(), &BigUint::from(50_000u32));
543 }
544
545 #[test]
550 fn quote_status_all_variants() {
551 use dto::QuoteStatus as Dto;
552 assert!(matches!(QuoteStatus::from(Dto::Success), QuoteStatus::Success));
553 assert!(matches!(QuoteStatus::from(Dto::NoRouteFound), QuoteStatus::NoRouteFound));
554 assert!(matches!(
555 QuoteStatus::from(Dto::InsufficientLiquidity),
556 QuoteStatus::InsufficientLiquidity
557 ));
558 assert!(matches!(QuoteStatus::from(Dto::Timeout), QuoteStatus::Timeout));
559 assert!(matches!(QuoteStatus::from(Dto::NotReady), QuoteStatus::NotReady));
560 assert!(matches!(QuoteStatus::from(Dto::PriceCheckFailed), QuoteStatus::PriceCheckFailed));
561 }
562
563 #[test]
568 fn block_info_from_dto() {
569 let dto_block = sample_dto_block();
570 let block = BlockInfo::from(dto_block);
571 assert_eq!(block.number(), 21_000_000);
572 assert_eq!(block.hash(), "0xdeadbeef");
573 assert_eq!(block.timestamp(), 1_730_000_000);
574 }
575
576 #[test]
581 fn quote_from_dto() {
582 let ds = sample_dto_order_quote();
583 let quote = order_quote_to_quote(ds, Bytes::new(), Bytes::new()).unwrap();
584 assert_eq!(quote.order_id(), "test-order-id");
585 assert!(matches!(quote.status(), QuoteStatus::Success));
586 assert!(matches!(quote.backend(), BackendKind::Fynd));
587 assert_eq!(quote.amount_in(), &BigUint::from(1_000u32));
588 assert_eq!(quote.amount_out(), &BigUint::from(999u32));
589 assert_eq!(quote.gas_estimate(), &BigUint::from(100_000u32));
590 assert_eq!(quote.amount_out_net_gas(), &BigUint::from(998u32));
591 assert_eq!(quote.price_impact_bps(), Some(5));
592 assert!(quote.token_out().is_empty());
594 assert!(quote.receiver().is_empty());
595 }
596
597 #[test]
602 fn order_try_from_client_encodes_addresses_as_tycho() {
603 let order = Order::new(
604 Bytes::copy_from_slice(&[0xaa; 20]),
605 Bytes::copy_from_slice(&[0xbb; 20]),
606 BigUint::from(1_000u32),
607 OrderSide::Sell,
608 Bytes::copy_from_slice(&[0xcc; 20]),
609 None,
610 );
611
612 let dto_order = dto::Order::try_from(order).unwrap();
613 assert_eq!(dto_order.token_in().as_ref(), &[0xaa; 20]);
614 assert_eq!(dto_order.token_out().as_ref(), &[0xbb; 20]);
615 assert_eq!(dto_order.sender().as_ref(), &[0xcc; 20]);
616 assert!(dto_order.receiver().is_none());
617 assert_eq!(dto_order.amount(), &BigUint::from(1_000u32));
618 }
619
620 #[test]
621 fn order_try_from_client_with_receiver() {
622 let order = Order::new(
623 Bytes::copy_from_slice(&[0xaa; 20]),
624 Bytes::copy_from_slice(&[0xbb; 20]),
625 BigUint::from(1u32),
626 OrderSide::Sell,
627 Bytes::copy_from_slice(&[0xcc; 20]),
628 Some(Bytes::copy_from_slice(&[0xdd; 20])),
629 );
630
631 let dto_order = dto::Order::try_from(order).unwrap();
632 let receiver = dto_order.receiver().unwrap();
633 assert_eq!(receiver.as_ref(), &[0xdd; 20]);
634 }
635
636 #[test]
637 fn order_try_from_client_invalid_address_length() {
638 let order = Order::new(
639 Bytes::copy_from_slice(&[0xaa; 4]), Bytes::copy_from_slice(&[0xbb; 20]),
641 BigUint::from(1u32),
642 OrderSide::Sell,
643 Bytes::copy_from_slice(&[0xcc; 20]),
644 None,
645 );
646 assert!(matches!(dto::Order::try_from(order), Err(FyndError::Protocol(_))));
647 }
648
649 #[test]
654 fn user_transfer_type_permit2_maps_correctly() {
655 let result = dto::UserTransferType::from(UserTransferType::TransferFromPermit2);
656 assert!(matches!(result, dto::UserTransferType::TransferFromPermit2));
657 }
658
659 #[test]
660 fn user_transfer_type_vault_funds_maps_correctly() {
661 let result = dto::UserTransferType::from(UserTransferType::UseVaultsFunds);
662 assert!(matches!(result, dto::UserTransferType::UseVaultsFunds));
663 }
664
665 #[test]
670 fn permit_details_try_from_happy_path() {
671 let details = PermitDetails::new(
672 Bytes::copy_from_slice(&[0xaa; 20]),
673 BigUint::from(1_000u32),
674 BigUint::from(9_999_999u32),
675 BigUint::from(0u32),
676 );
677 let dto_details = dto::PermitDetails::try_from(details).unwrap();
678 assert_eq!(dto_details.token().as_ref(), &[0xaa; 20]);
679 assert_eq!(dto_details.amount(), &BigUint::from(1_000u32));
680 assert_eq!(dto_details.expiration(), &BigUint::from(9_999_999u32));
681 assert_eq!(dto_details.nonce(), &BigUint::from(0u32));
682 }
683
684 #[test]
685 fn permit_details_try_from_invalid_token() {
686 let details = PermitDetails::new(
687 Bytes::copy_from_slice(&[0xaa; 4]), BigUint::from(1u32),
689 BigUint::from(1u32),
690 BigUint::from(0u32),
691 );
692 assert!(matches!(dto::PermitDetails::try_from(details), Err(FyndError::Protocol(_))));
693 }
694
695 #[test]
700 fn permit_single_try_from_happy_path() {
701 let details = PermitDetails::new(
702 Bytes::copy_from_slice(&[0xaa; 20]),
703 BigUint::from(500u32),
704 BigUint::from(1_000_000u32),
705 BigUint::from(1u32),
706 );
707 let permit = PermitSingle::new(
708 details,
709 Bytes::copy_from_slice(&[0xbb; 20]),
710 BigUint::from(2_000_000u32),
711 );
712 let dto_permit = dto::PermitSingle::try_from(permit).unwrap();
713 assert_eq!(dto_permit.spender().as_ref(), &[0xbb; 20]);
714 assert_eq!(dto_permit.sig_deadline(), &BigUint::from(2_000_000u32));
715 assert_eq!(dto_permit.details().amount(), &BigUint::from(500u32));
716 }
717
718 #[test]
723 fn encoding_options_try_from_with_permit2() {
724 use crate::types::{EncodingOptions, PermitDetails, PermitSingle};
725
726 let details = PermitDetails::new(
727 Bytes::copy_from_slice(&[0xaa; 20]),
728 BigUint::from(1_000u32),
729 BigUint::from(9_999_999u32),
730 BigUint::from(0u32),
731 );
732 let permit = PermitSingle::new(
733 details,
734 Bytes::copy_from_slice(&[0xbb; 20]),
735 BigUint::from(9_999_999u32),
736 );
737 let sig = Bytes::copy_from_slice(&[0xcc; 65]);
738 let opts = EncodingOptions::new(0.005)
739 .with_permit2(permit, sig.clone())
740 .unwrap();
741
742 let dto_opts = dto::EncodingOptions::try_from(opts).unwrap();
743 assert!(matches!(dto_opts.transfer_type(), dto::UserTransferType::TransferFromPermit2));
744 assert!(dto_opts.permit().is_some());
745 assert_eq!(
746 dto_opts
747 .permit2_signature()
748 .unwrap()
749 .as_ref(),
750 sig.as_ref()
751 );
752 }
753
754 #[test]
759 fn encoding_options_try_from_with_client_fee() {
760 use crate::types::{ClientFeeParams, EncodingOptions};
761
762 let fee = ClientFeeParams::new(
763 100,
764 Bytes::copy_from_slice(&[0x44; 20]),
765 BigUint::from(500_000u64),
766 1_893_456_000u64,
767 )
768 .with_signature(Bytes::copy_from_slice(&[0xAB; 65]));
769 let opts = EncodingOptions::new(0.01).with_client_fee(fee);
770
771 let dto_opts = dto::EncodingOptions::try_from(opts).unwrap();
772 assert!(dto_opts.client_fee_params().is_some());
773 let dto_fee = dto_opts.client_fee_params().unwrap();
774 assert_eq!(dto_fee.bps(), 100);
775 assert_eq!(*dto_fee.max_contribution(), BigUint::from(500_000u64));
776 assert_eq!(dto_fee.deadline(), 1_893_456_000u64);
777 assert_eq!(dto_fee.signature().len(), 65);
778 }
779
780 #[test]
781 fn encoding_options_try_from_without_client_fee() {
782 use crate::types::EncodingOptions;
783
784 let opts = EncodingOptions::new(0.005);
785 let dto_opts = dto::EncodingOptions::try_from(opts).unwrap();
786 assert!(dto_opts.client_fee_params().is_none());
787 }
788
789 #[test]
794 fn batch_quote_params_to_dto_empty_orders_errors() {
795 use crate::types::{BatchQuoteParams, QuoteOptions};
796
797 let params = BatchQuoteParams::new(vec![], QuoteOptions::default());
798 assert!(matches!(batch_quote_params_to_dto(params), Err(FyndError::Protocol(_))));
799 }
800
801 #[test]
802 fn batch_quote_params_to_dto_extracts_per_order_meta() {
803 use crate::types::{BatchQuoteParams, Order, OrderSide, QuoteOptions};
804
805 let token_in_a = Bytes::copy_from_slice(&[0xaa; 20]);
806 let token_out_a = Bytes::copy_from_slice(&[0xbb; 20]);
807 let sender_a = Bytes::copy_from_slice(&[0xcc; 20]);
808 let receiver_a = Bytes::copy_from_slice(&[0xdd; 20]);
809
810 let token_in_b = Bytes::copy_from_slice(&[0x11; 20]);
811 let token_out_b = Bytes::copy_from_slice(&[0x22; 20]);
812 let sender_b = Bytes::copy_from_slice(&[0x33; 20]);
813
814 let order_a = Order::new(
815 token_in_a,
816 token_out_a.clone(),
817 BigUint::from(1_000u32),
818 OrderSide::Sell,
819 sender_a.clone(),
820 Some(receiver_a.clone()),
821 );
822 let order_b = Order::new(
823 token_in_b,
824 token_out_b.clone(),
825 BigUint::from(2_000u32),
826 OrderSide::Sell,
827 sender_b.clone(),
828 None, );
830
831 let params = BatchQuoteParams::new(vec![order_a, order_b], QuoteOptions::default());
832 let (request, meta) = batch_quote_params_to_dto(params).unwrap();
833
834 assert_eq!(request.orders().len(), 2);
835 assert_eq!(meta.len(), 2);
836
837 let (tok_out_0, rec_0) = &meta[0];
838 assert_eq!(tok_out_0.as_ref(), token_out_a.as_ref());
839 assert_eq!(rec_0.as_ref(), receiver_a.as_ref());
840
841 let (tok_out_1, rec_1) = &meta[1];
842 assert_eq!(tok_out_1.as_ref(), token_out_b.as_ref());
843 assert_eq!(rec_1.as_ref(), sender_b.as_ref());
845 }
846
847 #[test]
852 fn map_quote_response_count_mismatch_errors() {
853 let oq = sample_dto_order_quote();
854 let dto_quote = dto::Quote::new(vec![oq], BigUint::from(100_000u32), 42);
855 let meta = vec![(Bytes::new(), Bytes::new()), (Bytes::new(), Bytes::new())];
857 assert!(matches!(map_quote_response(dto_quote, meta), Err(FyndError::Protocol(_))));
858 }
859
860 #[test]
861 fn map_quote_response_maps_per_order_meta_and_solve_time() {
862 let oq_a = sample_dto_order_quote();
863 let oq_b = sample_dto_order_quote();
864 let solve_ms = 77u64;
865 let dto_quote = dto::Quote::new(vec![oq_a, oq_b], BigUint::from(200_000u32), solve_ms);
866
867 let token_out_a = Bytes::copy_from_slice(&[0xaa; 20]);
868 let receiver_a = Bytes::copy_from_slice(&[0xbb; 20]);
869 let token_out_b = Bytes::copy_from_slice(&[0xcc; 20]);
870 let receiver_b = Bytes::copy_from_slice(&[0xdd; 20]);
871
872 let meta = vec![
873 (token_out_a.clone(), receiver_a.clone()),
874 (token_out_b.clone(), receiver_b.clone()),
875 ];
876
877 let quotes = map_quote_response(dto_quote, meta).unwrap();
878 assert_eq!(quotes.len(), 2);
879
880 assert_eq!(quotes[0].token_out().as_ref(), token_out_a.as_ref());
881 assert_eq!(quotes[0].receiver().as_ref(), receiver_a.as_ref());
882 assert_eq!(quotes[0].solve_time_ms(), solve_ms);
883
884 assert_eq!(quotes[1].token_out().as_ref(), token_out_b.as_ref());
885 assert_eq!(quotes[1].receiver().as_ref(), receiver_b.as_ref());
886 assert_eq!(quotes[1].solve_time_ms(), solve_ms);
887 }
888}