use alloy::{
primitives::{keccak256, U256},
sol_types::SolValue,
};
use bytes::Bytes;
use num_bigint::BigUint;
use crate::mapping::biguint_to_u256;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum UserTransferType {
#[default]
TransferFrom,
TransferFromPermit2,
UseVaultsFunds,
}
#[derive(Debug, Clone)]
pub struct PermitDetails {
pub(crate) token: bytes::Bytes,
pub(crate) amount: num_bigint::BigUint,
pub(crate) expiration: num_bigint::BigUint,
pub(crate) nonce: num_bigint::BigUint,
}
impl PermitDetails {
pub fn new(
token: bytes::Bytes,
amount: num_bigint::BigUint,
expiration: num_bigint::BigUint,
nonce: num_bigint::BigUint,
) -> Self {
Self { token, amount, expiration, nonce }
}
}
#[derive(Debug, Clone)]
pub struct PermitSingle {
pub(crate) details: PermitDetails,
pub(crate) spender: bytes::Bytes,
pub(crate) sig_deadline: num_bigint::BigUint,
}
impl PermitSingle {
pub fn new(
details: PermitDetails,
spender: bytes::Bytes,
sig_deadline: num_bigint::BigUint,
) -> Self {
Self { details, spender, sig_deadline }
}
pub fn eip712_signing_hash(
&self,
chain_id: u64,
permit2_address: &bytes::Bytes,
) -> Result<[u8; 32], crate::error::FyndError> {
use alloy::sol_types::{eip712_domain, SolStruct};
let permit2_addr = p2_bytes_to_address(permit2_address, "permit2_address")?;
let token = p2_bytes_to_address(&self.details.token, "token")?;
let spender = p2_bytes_to_address(&self.spender, "spender")?;
let amount = p2_biguint_to_uint160(&self.details.amount)?;
let expiration = p2_biguint_to_uint48(&self.details.expiration)?;
let nonce = p2_biguint_to_uint48(&self.details.nonce)?;
let sig_deadline = crate::mapping::biguint_to_u256(&self.sig_deadline);
let domain = eip712_domain! {
name: "Permit2",
chain_id: chain_id,
verifying_contract: permit2_addr,
};
#[allow(non_snake_case)]
let permit = permit2_sol::PermitSingle {
details: permit2_sol::PermitDetails { token, amount, expiration, nonce },
spender,
sigDeadline: sig_deadline,
};
Ok(permit.eip712_signing_hash(&domain).0)
}
}
#[derive(Debug, Clone)]
pub struct ClientFeeParams {
pub(crate) bps: u16,
pub(crate) receiver: Bytes,
pub(crate) max_contribution: BigUint,
pub(crate) deadline: u64,
pub(crate) signature: Option<Bytes>,
}
impl ClientFeeParams {
pub fn new(bps: u16, receiver: Bytes, max_contribution: BigUint, deadline: u64) -> Self {
Self { bps, receiver, max_contribution, deadline, signature: None }
}
pub fn with_signature(mut self, signature: Bytes) -> Self {
self.signature = Some(signature);
self
}
pub fn eip712_signing_hash(
&self,
chain_id: u64,
router_address: &Bytes,
) -> Result<[u8; 32], crate::error::FyndError> {
let router_addr = p2_bytes_to_address(router_address, "router_address")?;
let fee_receiver = p2_bytes_to_address(&self.receiver, "receiver")?;
let max_contrib = biguint_to_u256(&self.max_contribution);
let dl = U256::from(self.deadline);
let type_hash = keccak256(
b"ClientFee(uint16 clientFeeBps,address clientFeeReceiver,\
uint256 maxClientContribution,uint256 deadline)",
);
let domain_type_hash = keccak256(
b"EIP712Domain(string name,string version,\
uint256 chainId,address verifyingContract)",
);
let domain_separator = keccak256(
(
domain_type_hash,
keccak256(b"TychoRouter"),
keccak256(b"1"),
U256::from(chain_id),
router_addr,
)
.abi_encode(),
);
let struct_hash = keccak256(
(type_hash, U256::from(self.bps), fee_receiver, max_contrib, dl).abi_encode(),
);
let mut data = [0u8; 66];
data[0] = 0x19;
data[1] = 0x01;
data[2..34].copy_from_slice(domain_separator.as_ref());
data[34..66].copy_from_slice(struct_hash.as_ref());
Ok(keccak256(data).0)
}
}
mod permit2_sol {
use alloy::sol;
sol! {
struct PermitDetails {
address token;
uint160 amount;
uint48 expiration;
uint48 nonce;
}
struct PermitSingle {
PermitDetails details;
address spender;
uint256 sigDeadline;
}
}
}
fn p2_bytes_to_address(
b: &bytes::Bytes,
field: &str,
) -> Result<alloy::primitives::Address, crate::error::FyndError> {
let arr: [u8; 20] = b.as_ref().try_into().map_err(|_| {
crate::error::FyndError::Protocol(format!(
"expected 20-byte address for {field}, got {} bytes",
b.len()
))
})?;
Ok(alloy::primitives::Address::from(arr))
}
fn p2_biguint_to_uint160(
n: &num_bigint::BigUint,
) -> Result<alloy::primitives::Uint<160, 3>, crate::error::FyndError> {
let bytes = n.to_bytes_be();
if bytes.len() > 20 {
return Err(crate::error::FyndError::Protocol(format!(
"permit amount exceeds uint160 ({} bytes)",
bytes.len()
)));
}
let mut arr = [0u8; 20];
arr[20 - bytes.len()..].copy_from_slice(&bytes);
Ok(alloy::primitives::Uint::<160, 3>::from_be_bytes(arr))
}
fn p2_biguint_to_uint48(
n: &num_bigint::BigUint,
) -> Result<alloy::primitives::Uint<48, 1>, crate::error::FyndError> {
let bytes = n.to_bytes_be();
if bytes.len() > 6 {
return Err(crate::error::FyndError::Protocol(format!(
"permit value exceeds uint48 ({} bytes)",
bytes.len()
)));
}
let mut arr = [0u8; 6];
arr[6 - bytes.len()..].copy_from_slice(&bytes);
Ok(alloy::primitives::Uint::<48, 1>::from_be_bytes(arr))
}
#[derive(Debug, Clone)]
pub struct EncodingOptions {
pub(crate) slippage: f64,
pub(crate) transfer_type: UserTransferType,
pub(crate) permit: Option<PermitSingle>,
pub(crate) permit2_signature: Option<Bytes>,
pub(crate) client_fee_params: Option<ClientFeeParams>,
pub(crate) price_guard: Option<PriceGuardConfig>,
}
impl EncodingOptions {
pub fn new(slippage: f64) -> Self {
Self {
slippage,
transfer_type: UserTransferType::TransferFrom,
permit: None,
permit2_signature: None,
client_fee_params: None,
price_guard: None,
}
}
pub fn with_permit2(
mut self,
permit: PermitSingle,
signature: bytes::Bytes,
) -> Result<Self, crate::error::FyndError> {
if signature.len() != 65 {
return Err(crate::error::FyndError::Protocol(format!(
"Permit2 signature must be exactly 65 bytes, got {}",
signature.len()
)));
}
self.transfer_type = UserTransferType::TransferFromPermit2;
self.permit = Some(permit);
self.permit2_signature = Some(signature);
Ok(self)
}
pub fn with_vault_funds(mut self) -> Self {
self.transfer_type = UserTransferType::UseVaultsFunds;
self
}
pub fn with_client_fee(mut self, params: ClientFeeParams) -> Self {
self.client_fee_params = Some(params);
self
}
pub fn with_price_guard(mut self, config: PriceGuardConfig) -> Self {
self.price_guard = Some(config);
self
}
}
#[derive(Debug, Clone)]
pub struct Transaction {
to: Bytes,
value: BigUint,
data: Vec<u8>,
}
impl Transaction {
pub fn new(to: Bytes, value: BigUint, data: Vec<u8>) -> Self {
Self { to, value, data }
}
pub fn to(&self) -> &Bytes {
&self.to
}
pub fn value(&self) -> &BigUint {
&self.value
}
pub fn data(&self) -> &[u8] {
&self.data
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OrderSide {
Sell,
}
#[derive(Debug, Clone)]
pub struct Order {
token_in: Bytes,
token_out: Bytes,
amount: BigUint,
side: OrderSide,
sender: Bytes,
receiver: Option<Bytes>,
}
impl Order {
pub fn new(
token_in: Bytes,
token_out: Bytes,
amount: BigUint,
side: OrderSide,
sender: Bytes,
receiver: Option<Bytes>,
) -> Self {
Self { token_in, token_out, amount, side, sender, receiver }
}
pub fn token_in(&self) -> &Bytes {
&self.token_in
}
pub fn token_out(&self) -> &Bytes {
&self.token_out
}
pub fn amount(&self) -> &BigUint {
&self.amount
}
pub fn side(&self) -> OrderSide {
self.side
}
pub fn sender(&self) -> &Bytes {
&self.sender
}
pub fn receiver(&self) -> Option<&Bytes> {
self.receiver.as_ref()
}
}
pub use fynd_rpc_types::PriceGuardConfig;
#[derive(Debug, Clone, Default)]
pub struct QuoteOptions {
pub(crate) timeout_ms: Option<u64>,
pub(crate) min_responses: Option<usize>,
pub(crate) max_gas: Option<BigUint>,
pub(crate) encoding_options: Option<EncodingOptions>,
}
impl QuoteOptions {
pub fn with_timeout_ms(mut self, ms: u64) -> Self {
self.timeout_ms = Some(ms);
self
}
pub fn with_min_responses(mut self, n: usize) -> Self {
self.min_responses = Some(n);
self
}
pub fn with_max_gas(mut self, gas: BigUint) -> Self {
self.max_gas = Some(gas);
self
}
pub fn with_encoding_options(mut self, opts: EncodingOptions) -> Self {
self.encoding_options = Some(opts);
self
}
pub fn timeout_ms(&self) -> Option<u64> {
self.timeout_ms
}
pub fn min_responses(&self) -> Option<usize> {
self.min_responses
}
pub fn max_gas(&self) -> Option<&BigUint> {
self.max_gas.as_ref()
}
}
#[derive(Debug, Clone)]
pub struct QuoteParams {
pub(crate) order: Order,
pub(crate) options: QuoteOptions,
}
impl QuoteParams {
pub fn new(order: Order, options: QuoteOptions) -> Self {
Self { order, options }
}
}
#[derive(Debug, Clone)]
pub struct BatchQuoteParams {
pub(crate) orders: Vec<Order>,
pub(crate) options: QuoteOptions,
}
impl BatchQuoteParams {
pub fn new(orders: Vec<Order>, options: QuoteOptions) -> Self {
Self { orders, options }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BackendKind {
Fynd,
Turbine,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum QuoteStatus {
Success,
NoRouteFound,
InsufficientLiquidity,
Timeout,
NotReady,
PriceCheckFailed,
}
#[derive(Debug, Clone)]
pub struct BlockInfo {
number: u64,
hash: String,
timestamp: u64,
}
impl BlockInfo {
pub fn number(&self) -> u64 {
self.number
}
pub fn hash(&self) -> &str {
&self.hash
}
pub fn timestamp(&self) -> u64 {
self.timestamp
}
pub fn new(number: u64, hash: String, timestamp: u64) -> Self {
Self { number, hash, timestamp }
}
}
#[derive(Debug, Clone)]
pub struct Swap {
component_id: String,
protocol: String,
token_in: Bytes,
token_out: Bytes,
amount_in: BigUint,
amount_out: BigUint,
gas_estimate: BigUint,
#[allow(dead_code)]
split: f64,
}
impl Swap {
pub fn component_id(&self) -> &str {
&self.component_id
}
pub fn protocol(&self) -> &str {
&self.protocol
}
pub fn token_in(&self) -> &Bytes {
&self.token_in
}
pub fn token_out(&self) -> &Bytes {
&self.token_out
}
pub fn amount_in(&self) -> &BigUint {
&self.amount_in
}
pub fn amount_out(&self) -> &BigUint {
&self.amount_out
}
pub fn gas_estimate(&self) -> &BigUint {
&self.gas_estimate
}
#[allow(clippy::too_many_arguments)]
pub fn new(
component_id: String,
protocol: String,
token_in: Bytes,
token_out: Bytes,
amount_in: BigUint,
amount_out: BigUint,
gas_estimate: BigUint,
split: f64,
) -> Self {
Self {
component_id,
protocol,
token_in,
token_out,
amount_in,
amount_out,
gas_estimate,
split,
}
}
}
#[derive(Debug, Clone)]
pub struct Route {
swaps: Vec<Swap>,
}
impl Route {
pub fn swaps(&self) -> &[Swap] {
&self.swaps
}
pub fn new(swaps: Vec<Swap>) -> Self {
Self { swaps }
}
}
#[derive(Debug, Clone)]
pub struct FeeBreakdown {
router_fee: BigUint,
client_fee: BigUint,
max_slippage: BigUint,
min_amount_received: BigUint,
}
impl FeeBreakdown {
pub(crate) fn new(
router_fee: BigUint,
client_fee: BigUint,
max_slippage: BigUint,
min_amount_received: BigUint,
) -> Self {
Self { router_fee, client_fee, max_slippage, min_amount_received }
}
pub fn router_fee(&self) -> &BigUint {
&self.router_fee
}
pub fn client_fee(&self) -> &BigUint {
&self.client_fee
}
pub fn max_slippage(&self) -> &BigUint {
&self.max_slippage
}
pub fn min_amount_received(&self) -> &BigUint {
&self.min_amount_received
}
}
#[derive(Debug, Clone)]
pub struct Quote {
order_id: String,
status: QuoteStatus,
backend: BackendKind,
route: Option<Route>,
amount_in: BigUint,
amount_out: BigUint,
gas_estimate: BigUint,
amount_out_net_gas: BigUint,
price_impact_bps: Option<i32>,
block: BlockInfo,
token_out: Bytes,
receiver: Bytes,
transaction: Option<Transaction>,
fee_breakdown: Option<FeeBreakdown>,
pub(crate) solve_time_ms: u64,
}
impl Quote {
pub fn order_id(&self) -> &str {
&self.order_id
}
pub fn status(&self) -> QuoteStatus {
self.status
}
pub fn backend(&self) -> BackendKind {
self.backend
}
pub fn route(&self) -> Option<&Route> {
self.route.as_ref()
}
pub fn amount_in(&self) -> &BigUint {
&self.amount_in
}
pub fn amount_out(&self) -> &BigUint {
&self.amount_out
}
pub fn gas_estimate(&self) -> &BigUint {
&self.gas_estimate
}
pub fn amount_out_net_gas(&self) -> &BigUint {
&self.amount_out_net_gas
}
pub fn price_impact_bps(&self) -> Option<i32> {
self.price_impact_bps
}
pub fn block(&self) -> &BlockInfo {
&self.block
}
pub fn token_out(&self) -> &Bytes {
&self.token_out
}
pub fn receiver(&self) -> &Bytes {
&self.receiver
}
pub fn transaction(&self) -> Option<&Transaction> {
self.transaction.as_ref()
}
pub fn fee_breakdown(&self) -> Option<&FeeBreakdown> {
self.fee_breakdown.as_ref()
}
pub fn solve_time_ms(&self) -> u64 {
self.solve_time_ms
}
#[allow(clippy::too_many_arguments)]
pub fn new(
order_id: String,
status: QuoteStatus,
backend: BackendKind,
route: Option<Route>,
amount_in: BigUint,
amount_out: BigUint,
gas_estimate: BigUint,
amount_out_net_gas: BigUint,
price_impact_bps: Option<i32>,
block: BlockInfo,
token_out: Bytes,
receiver: Bytes,
transaction: Option<Transaction>,
fee_breakdown: Option<FeeBreakdown>,
) -> Self {
Self {
order_id,
status,
backend,
route,
amount_in,
amount_out,
gas_estimate,
amount_out_net_gas,
price_impact_bps,
block,
token_out,
receiver,
transaction,
fee_breakdown,
solve_time_ms: 0,
}
}
}
#[derive(Debug, Clone)]
pub struct InstanceInfo {
router_address: bytes::Bytes,
permit2_address: bytes::Bytes,
chain_id: u64,
}
impl InstanceInfo {
pub(crate) fn new(
router_address: bytes::Bytes,
permit2_address: bytes::Bytes,
chain_id: u64,
) -> Self {
Self { router_address, permit2_address, chain_id }
}
pub fn router_address(&self) -> &bytes::Bytes {
&self.router_address
}
pub fn permit2_address(&self) -> &bytes::Bytes {
&self.permit2_address
}
pub fn chain_id(&self) -> u64 {
self.chain_id
}
}
#[derive(Debug, Clone)]
pub struct HealthStatus {
healthy: bool,
last_update_ms: u64,
num_solver_pools: usize,
derived_data_ready: bool,
gas_price_age_ms: Option<u64>,
}
impl HealthStatus {
pub fn healthy(&self) -> bool {
self.healthy
}
pub fn last_update_ms(&self) -> u64 {
self.last_update_ms
}
pub fn num_solver_pools(&self) -> usize {
self.num_solver_pools
}
pub fn derived_data_ready(&self) -> bool {
self.derived_data_ready
}
pub fn gas_price_age_ms(&self) -> Option<u64> {
self.gas_price_age_ms
}
pub(crate) fn new(
healthy: bool,
last_update_ms: u64,
num_solver_pools: usize,
derived_data_ready: bool,
gas_price_age_ms: Option<u64>,
) -> Self {
Self { healthy, last_update_ms, num_solver_pools, derived_data_ready, gas_price_age_ms }
}
}
#[cfg(test)]
mod tests {
use num_bigint::BigUint;
use super::*;
fn addr(bytes: &[u8; 20]) -> Bytes {
Bytes::copy_from_slice(bytes)
}
#[test]
fn order_new_and_getters() {
let token_in = addr(&[0xaa; 20]);
let token_out = addr(&[0xbb; 20]);
let amount = BigUint::from(1_000_000u64);
let sender = addr(&[0xcc; 20]);
let order = Order::new(
token_in.clone(),
token_out.clone(),
amount.clone(),
OrderSide::Sell,
sender.clone(),
None,
);
assert_eq!(order.token_in(), &token_in);
assert_eq!(order.token_out(), &token_out);
assert_eq!(order.amount(), &amount);
assert_eq!(order.sender(), &sender);
assert!(order.receiver().is_none());
assert_eq!(order.side(), OrderSide::Sell);
}
#[test]
fn order_with_explicit_receiver() {
let receiver = Bytes::copy_from_slice(&[0xdd; 20]);
let order = Order::new(
Bytes::copy_from_slice(&[0xaa; 20]),
Bytes::copy_from_slice(&[0xbb; 20]),
BigUint::from(1u32),
OrderSide::Sell,
Bytes::copy_from_slice(&[0xcc; 20]),
Some(receiver.clone()),
);
assert_eq!(order.receiver(), Some(&receiver));
}
#[test]
fn quote_options_builder() {
let opts = QuoteOptions::default()
.with_timeout_ms(500)
.with_min_responses(2)
.with_max_gas(BigUint::from(1_000_000u64));
assert_eq!(opts.timeout_ms(), Some(500));
assert_eq!(opts.min_responses(), Some(2));
assert_eq!(opts.max_gas(), Some(&BigUint::from(1_000_000u64)));
}
#[test]
fn quote_options_default_all_none() {
let opts = QuoteOptions::default();
assert!(opts.timeout_ms().is_none());
assert!(opts.min_responses().is_none());
assert!(opts.max_gas().is_none());
}
#[test]
fn encoding_options_with_permit2_sets_fields() {
let token = Bytes::copy_from_slice(&[0xaa; 20]);
let spender = Bytes::copy_from_slice(&[0xbb; 20]);
let sig = Bytes::copy_from_slice(&[0xcc; 65]);
let details = PermitDetails::new(
token,
BigUint::from(1_000u32),
BigUint::from(9_999_999u32),
BigUint::from(0u32),
);
let permit = PermitSingle::new(details, spender, BigUint::from(9_999_999u32));
let opts = EncodingOptions::new(0.005)
.with_permit2(permit, sig.clone())
.unwrap();
assert_eq!(opts.transfer_type, UserTransferType::TransferFromPermit2);
assert!(opts.permit.is_some());
assert_eq!(opts.permit2_signature.as_ref().unwrap(), &sig);
}
#[test]
fn encoding_options_with_permit2_rejects_wrong_signature_length() {
let details = PermitDetails::new(
Bytes::copy_from_slice(&[0xaa; 20]),
BigUint::from(1_000u32),
BigUint::from(9_999_999u32),
BigUint::from(0u32),
);
let permit = PermitSingle::new(
details,
Bytes::copy_from_slice(&[0xbb; 20]),
BigUint::from(9_999_999u32),
);
let bad_sig = Bytes::copy_from_slice(&[0xcc; 64]); assert!(matches!(
EncodingOptions::new(0.005).with_permit2(permit, bad_sig),
Err(crate::error::FyndError::Protocol(_))
));
}
#[test]
fn encoding_options_with_vault_funds_sets_variant() {
let opts = EncodingOptions::new(0.005).with_vault_funds();
assert_eq!(opts.transfer_type, UserTransferType::UseVaultsFunds);
assert!(opts.permit.is_none());
assert!(opts.permit2_signature.is_none());
}
fn sample_permit_single() -> PermitSingle {
let details = PermitDetails::new(
Bytes::copy_from_slice(&[0xaa; 20]),
BigUint::from(1_000u32),
BigUint::from(9_999_999u32),
BigUint::from(0u32),
);
PermitSingle::new(details, Bytes::copy_from_slice(&[0xbb; 20]), BigUint::from(9_999_999u32))
}
#[test]
fn eip712_signing_hash_returns_32_bytes() {
let permit = sample_permit_single();
let permit2_addr = Bytes::copy_from_slice(&[0xcc; 20]);
let hash = permit
.eip712_signing_hash(1, &permit2_addr)
.unwrap();
assert_eq!(hash.len(), 32);
assert_ne!(hash, [0u8; 32]);
}
#[test]
fn eip712_signing_hash_is_deterministic() {
let permit2_addr = Bytes::copy_from_slice(&[0xcc; 20]);
let h1 = sample_permit_single()
.eip712_signing_hash(1, &permit2_addr)
.unwrap();
let h2 = sample_permit_single()
.eip712_signing_hash(1, &permit2_addr)
.unwrap();
assert_eq!(h1, h2);
}
#[test]
fn eip712_signing_hash_differs_by_chain_id() {
let permit2_addr = Bytes::copy_from_slice(&[0xcc; 20]);
let h1 = sample_permit_single()
.eip712_signing_hash(1, &permit2_addr)
.unwrap();
let h137 = sample_permit_single()
.eip712_signing_hash(137, &permit2_addr)
.unwrap();
assert_ne!(h1, h137);
}
#[test]
fn eip712_signing_hash_invalid_permit2_address() {
let permit = sample_permit_single();
let bad_addr = Bytes::copy_from_slice(&[0xcc; 4]);
assert!(matches!(
permit.eip712_signing_hash(1, &bad_addr),
Err(crate::error::FyndError::Protocol(_))
));
}
#[test]
fn eip712_signing_hash_invalid_token_address() {
let details = PermitDetails::new(
Bytes::copy_from_slice(&[0xaa; 4]), BigUint::from(1u32),
BigUint::from(1u32),
BigUint::from(0u32),
);
let permit =
PermitSingle::new(details, Bytes::copy_from_slice(&[0xbb; 20]), BigUint::from(1u32));
let permit2_addr = Bytes::copy_from_slice(&[0xcc; 20]);
assert!(matches!(
permit.eip712_signing_hash(1, &permit2_addr),
Err(crate::error::FyndError::Protocol(_))
));
}
#[test]
fn eip712_signing_hash_amount_exceeds_uint160() {
let oversized_amount = BigUint::from_bytes_be(&[0x01; 21]);
let details = PermitDetails::new(
Bytes::copy_from_slice(&[0xaa; 20]),
oversized_amount,
BigUint::from(1u32),
BigUint::from(0u32),
);
let permit =
PermitSingle::new(details, Bytes::copy_from_slice(&[0xbb; 20]), BigUint::from(1u32));
let permit2_addr = Bytes::copy_from_slice(&[0xcc; 20]);
assert!(matches!(
permit.eip712_signing_hash(1, &permit2_addr),
Err(crate::error::FyndError::Protocol(_))
));
}
fn sample_fee_receiver() -> Bytes {
Bytes::copy_from_slice(&[0x44; 20])
}
fn sample_router_address() -> Bytes {
Bytes::copy_from_slice(&[0x33; 20])
}
fn sample_fee_params(bps: u16, receiver: Bytes) -> ClientFeeParams {
ClientFeeParams::new(bps, receiver, BigUint::ZERO, 1_893_456_000)
}
#[test]
fn client_fee_with_client_fee_sets_fields() {
let fee = ClientFeeParams::new(
100,
sample_fee_receiver(),
BigUint::from(500_000u64),
1_893_456_000,
);
let opts = EncodingOptions::new(0.01).with_client_fee(fee);
assert!(opts.client_fee_params.is_some());
let stored = opts.client_fee_params.as_ref().unwrap();
assert_eq!(stored.bps, 100);
assert_eq!(stored.max_contribution, BigUint::from(500_000u64));
}
#[test]
fn client_fee_signing_hash_returns_32_bytes() {
let fee = sample_fee_params(100, sample_fee_receiver());
let hash = fee
.eip712_signing_hash(1, &sample_router_address())
.unwrap();
assert_eq!(hash.len(), 32);
assert_ne!(hash, [0u8; 32]);
}
#[test]
fn client_fee_signing_hash_is_deterministic() {
let fee = sample_fee_params(100, sample_fee_receiver());
let h1 = fee
.eip712_signing_hash(1, &sample_router_address())
.unwrap();
let h2 = fee
.eip712_signing_hash(1, &sample_router_address())
.unwrap();
assert_eq!(h1, h2);
}
#[test]
fn client_fee_signing_hash_differs_by_chain_id() {
let fee = sample_fee_params(100, sample_fee_receiver());
let h1 = fee
.eip712_signing_hash(1, &sample_router_address())
.unwrap();
let h137 = fee
.eip712_signing_hash(137, &sample_router_address())
.unwrap();
assert_ne!(h1, h137);
}
#[test]
fn client_fee_signing_hash_differs_by_bps() {
let h100 = sample_fee_params(100, sample_fee_receiver())
.eip712_signing_hash(1, &sample_router_address())
.unwrap();
let h200 = sample_fee_params(200, sample_fee_receiver())
.eip712_signing_hash(1, &sample_router_address())
.unwrap();
assert_ne!(h100, h200);
}
#[test]
fn client_fee_signing_hash_differs_by_receiver() {
let other_receiver = Bytes::copy_from_slice(&[0x55; 20]);
let h1 = sample_fee_params(100, sample_fee_receiver())
.eip712_signing_hash(1, &sample_router_address())
.unwrap();
let h2 = sample_fee_params(100, other_receiver)
.eip712_signing_hash(1, &sample_router_address())
.unwrap();
assert_ne!(h1, h2);
}
#[test]
fn client_fee_signing_hash_rejects_bad_receiver_address() {
let bad_addr = Bytes::copy_from_slice(&[0x44; 4]);
let fee = sample_fee_params(100, bad_addr);
assert!(matches!(
fee.eip712_signing_hash(1, &sample_router_address()),
Err(crate::error::FyndError::Protocol(_))
));
}
#[test]
fn client_fee_signing_hash_rejects_bad_router_address() {
let bad_addr = Bytes::copy_from_slice(&[0x33; 4]);
let fee = sample_fee_params(100, sample_fee_receiver());
assert!(matches!(
fee.eip712_signing_hash(1, &bad_addr),
Err(crate::error::FyndError::Protocol(_))
));
}
}