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