Skip to main content

fynd_client/
mapping.rs

1//! Conversions between client types and the Fynd RPC server's DTO types.
2//!
3//! Uses `fynd-rpc-types` directly for the DTO format, providing compile-time
4//! compatibility guarantees with the server.
5
6use 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};
17// ============================================================================
18// ADDRESS CONVERSION HELPERS
19// ============================================================================
20
21pub(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
31/// Wrap a client `bytes::Bytes` address as a DTO address, validating the 20-byte length.
32fn 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
39/// Unwrap a DTO address back to a client `bytes::Bytes`.
40fn dto_addr_to_bytes(b: dto::Bytes) -> bytes::Bytes {
41    b.0
42}
43
44// ============================================================================
45// PRIMITIVE CONVERSIONS
46// ============================================================================
47
48/// Convert a [`num_bigint::BigUint`] to an [`alloy::primitives::U256`].
49pub(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
53// ============================================================================
54// CLIENT TYPES → DTO FORMAT
55// ============================================================================
56
57pub(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
178// ============================================================================
179// DTO FORMAT → CLIENT TYPES
180// ============================================================================
181
182pub(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
332// ============================================================================
333// ERROR CONVERSION
334// ============================================================================
335
336pub(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    // -----------------------------------------------------------------------
392    // biguint_to_u256
393    // -----------------------------------------------------------------------
394
395    #[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    // -----------------------------------------------------------------------
409    // Transaction conversion
410    // -----------------------------------------------------------------------
411
412    #[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    // -----------------------------------------------------------------------
427    // bytes_to_alloy_address
428    // -----------------------------------------------------------------------
429
430    #[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    // -----------------------------------------------------------------------
444    // Swap conversion
445    // -----------------------------------------------------------------------
446
447    #[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    // -----------------------------------------------------------------------
460    // QuoteStatus conversion
461    // -----------------------------------------------------------------------
462
463    #[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    // -----------------------------------------------------------------------
477    // BlockInfo conversion
478    // -----------------------------------------------------------------------
479
480    #[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    // -----------------------------------------------------------------------
490    // OrderQuote conversion
491    // -----------------------------------------------------------------------
492
493    #[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        // token_out and receiver are left empty until populated by quote()
506        assert!(quote.token_out().is_empty());
507        assert!(quote.receiver().is_empty());
508    }
509
510    // -----------------------------------------------------------------------
511    // Order → dto::Order conversion
512    // -----------------------------------------------------------------------
513
514    #[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]), // wrong length
553            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    // -----------------------------------------------------------------------
563    // UserTransferType mapping
564    // -----------------------------------------------------------------------
565
566    #[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    // -----------------------------------------------------------------------
579    // PermitDetails TryFrom
580    // -----------------------------------------------------------------------
581
582    #[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]), // wrong length
601            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    // -----------------------------------------------------------------------
609    // PermitSingle TryFrom
610    // -----------------------------------------------------------------------
611
612    #[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    // -----------------------------------------------------------------------
632    // EncodingOptions TryFrom with permit2
633    // -----------------------------------------------------------------------
634
635    #[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    // -----------------------------------------------------------------------
668    // EncodingOptions TryFrom with client fee
669    // -----------------------------------------------------------------------
670
671    #[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}