1use std::collections::HashMap;
38use std::sync::Arc;
39use std::time::{Duration, SystemTime, UNIX_EPOCH};
40
41use tokio::sync::{Mutex, OnceCell};
42
43use crate::abi::{
44 address_word, bool_word, decode_bool_at, decode_uint256_at, encode_call, normalize_address,
45 uint256_word, SELECTOR_ALLOWANCE, SELECTOR_APPROVE, SELECTOR_BALANCE_OF,
46 SELECTOR_GET_FIRST_BUY_FEE, SELECTOR_GET_MAX_SELLABLE_ETH, SELECTOR_GET_SWAP_OUTPUT,
47 SELECTOR_PAUSED, SELECTOR_PLATFORM_BUY_FEE_BPS, SELECTOR_SWAP_ETH_FOR_EXACT_TOKENS,
48 SELECTOR_SWAP_EXACT_ETH_FOR_TOKENS, SELECTOR_SWAP_EXACT_TOKENS_FOR_ETH,
49 SELECTOR_TOKEN_OWNER_FEE_BPS, SELECTOR_TOKEN_POOLS, U256,
50};
51use crate::addresses::{deployment_for_chain_id, deployment_for_network, Deployment, DEPLOYMENTS};
52use crate::models::ChainInfo;
53use crate::revert::{graduated_error, revert_error, revert_selector, SELECTOR_ALREADY_LAUNCHED};
54use crate::rpc::{CallOutcome, RpcClient, DEFAULT_BLOCK_TAG};
55use crate::{Error, FyuzClient};
56
57const BPS_DIVISOR: u32 = 10_000;
59
60pub const DEFAULT_DEADLINE_SECONDS: u64 = 120;
62
63pub const MAX_DEADLINE_SECONDS: u64 = 365 * 24 * 60 * 60;
70
71pub const DEFAULT_NETWORK: &str = "bsc";
73
74fn invalid(message: impl Into<String>) -> Error {
75 Error::InvalidArgument {
76 message: message.into(),
77 }
78}
79
80#[derive(Debug, Clone, PartialEq, Eq)]
82pub struct UnsignedTransaction {
83 pub chain_id: i64,
86 pub to: String,
88 pub data: String,
90 pub value: U256,
92 pub from: Option<String>,
94}
95
96#[derive(Debug, Clone)]
98pub struct BuyQuote {
99 pub network: String,
101 pub token: String,
103 pub amount_in_wei: U256,
105 pub first_buy_fee_wei: U256,
107 pub value_wei: U256,
109 pub amount_out_wei: U256,
111 pub price_impact_bps: u64,
113}
114
115#[derive(Debug, Clone)]
117pub struct SellQuote {
118 pub network: String,
120 pub token: String,
122 pub amount_in_wei: U256,
124 pub amount_out_wei: U256,
127 pub price_impact_bps: u64,
129}
130
131#[derive(Debug, Clone)]
133pub struct BuiltBuy {
134 pub transaction: UnsignedTransaction,
136 pub quote: BuyQuote,
138 pub limit_wei: U256,
140 pub deadline: u64,
142}
143
144#[derive(Debug, Clone)]
146pub struct BuiltSell {
147 pub transaction: UnsignedTransaction,
149 pub quote: SellQuote,
151 pub limit_wei: U256,
153 pub deadline: u64,
155}
156
157#[derive(Debug, Clone, Default)]
159struct Limits {
160 slippage_bps: Option<u32>,
161 limit_wei: Option<U256>,
162 deadline: Option<u64>,
163 deadline_seconds: Option<u64>,
164 from: Option<String>,
165}
166
167macro_rules! limit_builders {
168 () => {
169 pub fn slippage_bps(mut self, bps: u32) -> Self {
172 self.limits.slippage_bps = Some(bps);
173 self
174 }
175
176 pub fn limit_wei(mut self, limit: U256) -> Self {
178 self.limits.limit_wei = Some(limit);
179 self
180 }
181
182 pub fn deadline(mut self, unix_seconds: u64) -> Self {
185 self.limits.deadline = Some(unix_seconds);
186 self
187 }
188
189 pub fn deadline_seconds(mut self, seconds: u64) -> Self {
191 self.limits.deadline_seconds = Some(seconds);
192 self
193 }
194
195 pub fn from(mut self, address: impl Into<String>) -> Self {
197 self.limits.from = Some(address.into());
198 self
199 }
200
201 pub fn network(mut self, network: impl Into<String>) -> Self {
203 self.network = network.into();
204 self
205 }
206 };
207}
208
209#[derive(Debug, Clone)]
211pub struct BuyParams {
212 token: String,
213 amount_wei: U256,
214 network: String,
215 limits: Limits,
216}
217
218impl BuyParams {
219 pub fn new(token: impl Into<String>, amount_wei: U256) -> Self {
222 Self {
223 token: token.into(),
224 amount_wei,
225 network: DEFAULT_NETWORK.to_string(),
226 limits: Limits::default(),
227 }
228 }
229
230 limit_builders!();
231}
232
233#[derive(Debug, Clone)]
235pub struct BuyExactTokensParams {
236 token: String,
237 amount_out_wei: U256,
238 network: String,
239 limits: Limits,
240}
241
242impl BuyExactTokensParams {
243 pub fn new(token: impl Into<String>, amount_out_wei: U256) -> Self {
245 Self {
246 token: token.into(),
247 amount_out_wei,
248 network: DEFAULT_NETWORK.to_string(),
249 limits: Limits::default(),
250 }
251 }
252
253 limit_builders!();
254}
255
256#[derive(Debug, Clone)]
258pub struct SellParams {
259 token: String,
260 amount_wei: U256,
261 network: String,
262 limits: Limits,
263}
264
265impl SellParams {
266 pub fn new(token: impl Into<String>, amount_wei: U256) -> Self {
268 Self {
269 token: token.into(),
270 amount_wei,
271 network: DEFAULT_NETWORK.to_string(),
272 limits: Limits::default(),
273 }
274 }
275
276 limit_builders!();
277}
278
279#[derive(Debug)]
282pub struct TradeApi<'a> {
283 client: &'a FyuzClient,
284}
285
286impl<'a> TradeApi<'a> {
287 pub(crate) fn new(client: &'a FyuzClient) -> Self {
288 Self { client }
289 }
290
291 pub async fn chain(&self, network: &str) -> Result<ChainInfo, Error> {
303 if let Some(deployment) = deployment_for_network(network) {
308 if deployment.api_network.is_none() {
309 return Ok(chain_from(deployment));
310 }
311 }
312
313 let chains = self
314 .client
315 .trade_state()
316 .chains
317 .get_or_try_init(|| async { Ok::<_, Error>(self.client.get_config().await?.chains) })
318 .await?;
319
320 for chain in chains {
321 if chain.network.as_deref() != Some(network) && chain.chain_id.to_string() != network {
322 continue;
323 }
324
325 let mut chain = chain.clone();
326 chain.contract_address = match chain.contract_address.as_deref() {
332 Some(address) => Some(normalize_address("contractAddress", address)?),
333 None => deployment_for_chain_id(chain.chain_id).map(|d| d.fyuz.to_string()),
334 };
335 if chain.contract_address.is_none() {
336 return Err(invalid(format!(
337 "the API did not publish a contract address for {network:?} and this SDK \
338 has none on file for chain id {}, so no transaction can be built",
339 chain.chain_id
340 )));
341 }
342 return Ok(chain);
343 }
344
345 if let Some(deployment) = deployment_for_network(network) {
346 return Ok(chain_from(deployment));
347 }
348
349 let known: Vec<String> = chains
350 .iter()
351 .map(|c| c.network.clone().unwrap_or_else(|| c.chain_id.to_string()))
352 .chain(DEPLOYMENTS.iter().map(|d| d.network.to_string()))
353 .collect();
354 Err(invalid(format!(
355 "unknown network {network:?}; the API serves: {}",
356 known.join(", ")
357 )))
358 }
359
360 async fn resolve(&self, network: &str) -> Result<Resolved, Error> {
367 let chain = self.chain(network).await?;
368 let contract = chain
369 .contract_address
370 .ok_or_else(|| invalid(format!("no contract address is known for {network:?}")))?;
371 Ok(Resolved {
372 chain_id: chain.chain_id,
373 contract,
374 rpc_url: chain.rpc_url,
375 })
376 }
377
378 async fn rpc(&self, network: &str) -> Result<Arc<RpcClient>, Error> {
380 {
381 let cache = self.client.trade_state().rpcs.lock().await;
382 if let Some(existing) = cache.get(network) {
383 return Ok(Arc::clone(existing));
384 }
385 }
386
387 let chain = self.resolve(network).await?;
388 let url = self
389 .client
390 .rpc_url()
391 .map(str::to_string)
392 .or_else(|| chain.rpc_url.clone())
393 .ok_or_else(|| {
394 invalid(format!(
395 "no RPC endpoint for {network:?}: the API published none, and none was \
396 configured with FyuzClientBuilder::rpc_url"
397 ))
398 })?;
399
400 let client = Arc::new(
405 RpcClient::with_http(url, self.client.rpc_http().clone())
406 .block(self.client.rpc_block().unwrap_or(DEFAULT_BLOCK_TAG)),
407 );
408
409 let mut cache = self.client.trade_state().rpcs.lock().await;
410 Ok(Arc::clone(
411 cache.entry(network.to_string()).or_insert(client),
412 ))
413 }
414
415 async fn call_contract(&self, network: &str, data: &str) -> Result<CallOutcome, Error> {
417 let chain = self.resolve(network).await?;
418 let rpc = self.rpc(network).await?;
419 rpc.call(&chain.contract, data, None).await
420 }
421
422 pub async fn is_graduated(&self, network: &str, token: &str) -> Result<bool, Error> {
430 Ok(self.pool_state(network, token).await?.launched)
431 }
432
433 async fn pool_state(&self, network: &str, token: &str) -> Result<PoolState, Error> {
441 let token = normalize_address("token", token)?;
442 let data = encode_call(SELECTOR_TOKEN_POOLS, &[address_word(&token)?]);
443
444 match self.call_contract(network, &data).await? {
445 CallOutcome::Ok(result) => Ok(PoolState {
449 token_reserve: decode_uint256_at(&result, 1)?,
450 virtual_eth_reserve: decode_uint256_at(&result, 2)?,
451 virtual_token_reserve: decode_uint256_at(&result, 3)?,
452 launched: decode_bool_at(&result, 7)?,
453 }),
454 CallOutcome::Reverted(revert) => {
455 Err(revert_error(&revert, &format!("tokenPools({token})")))
456 }
457 }
458 }
459
460 pub async fn is_paused(&self, network: &str) -> Result<bool, Error> {
468 let data = encode_call(SELECTOR_PAUSED, &[]);
469
470 match self.call_contract(network, &data).await? {
471 CallOutcome::Ok(result) => decode_bool_at(&result, 0),
472 CallOutcome::Reverted(revert) => Err(revert_error(&revert, "paused()")),
473 }
474 }
475
476 pub async fn first_buy_fee(&self, network: &str, token: &str) -> Result<U256, Error> {
478 let token = normalize_address("token", token)?;
479 let data = encode_call(SELECTOR_GET_FIRST_BUY_FEE, &[address_word(&token)?]);
480
481 match self.call_contract(network, &data).await? {
482 CallOutcome::Ok(result) => decode_uint256_at(&result, 0),
483 CallOutcome::Reverted(revert) => {
484 Err(revert_error(&revert, &format!("getFirstBuyFee({token})")))
485 }
486 }
487 }
488
489 pub async fn max_sellable_wei(&self, network: &str, token: &str) -> Result<U256, Error> {
491 let token = normalize_address("token", token)?;
492 let data = encode_call(SELECTOR_GET_MAX_SELLABLE_ETH, &[address_word(&token)?]);
493
494 match self.call_contract(network, &data).await? {
495 CallOutcome::Ok(result) => decode_uint256_at(&result, 0),
496 CallOutcome::Reverted(revert) => Err(revert_error(
497 &revert,
498 &format!("getMaxSellableETH({token})"),
499 )),
500 }
501 }
502
503 pub async fn allowance(&self, network: &str, token: &str, owner: &str) -> Result<U256, Error> {
505 let token = normalize_address("token", token)?;
506 let owner = normalize_address("owner", owner)?;
507 let chain = self.resolve(network).await?;
508 let rpc = self.rpc(network).await?;
509
510 let data = encode_call(
511 SELECTOR_ALLOWANCE,
512 &[address_word(&owner)?, address_word(&chain.contract)?],
513 );
514 let result = rpc
515 .call_or_error(&token, &data, &format!("allowance({owner})"))
516 .await?;
517 decode_uint256_at(&result, 0)
518 }
519
520 pub async fn balance_of(&self, network: &str, token: &str, owner: &str) -> Result<U256, Error> {
522 let token = normalize_address("token", token)?;
523 let owner = normalize_address("owner", owner)?;
524 let rpc = self.rpc(network).await?;
525
526 let data = encode_call(SELECTOR_BALANCE_OF, &[address_word(&owner)?]);
527 let result = rpc
528 .call_or_error(&token, &data, &format!("balanceOf({owner})"))
529 .await?;
530 decode_uint256_at(&result, 0)
531 }
532
533 async fn swap_output(
537 &self,
538 network: &str,
539 token: &str,
540 amount_in: U256,
541 is_eth_input: bool,
542 ) -> Result<(U256, u64), Error> {
543 let data = encode_call(
544 SELECTOR_GET_SWAP_OUTPUT,
545 &[
546 address_word(token)?,
547 uint256_word(amount_in),
548 bool_word(is_eth_input),
549 ],
550 );
551
552 match self.call_contract(network, &data).await? {
553 CallOutcome::Ok(result) => {
554 let amount_out = decode_uint256_at(&result, 0)?;
555 let impact = decode_uint256_at(&result, 1)?;
556 let impact = impact.to_string().parse::<u64>().unwrap_or(u64::MAX);
557 Ok((amount_out, impact))
558 }
559 CallOutcome::Reverted(revert) => {
560 if revert_selector(&revert).as_deref() == Some(SELECTOR_ALREADY_LAUNCHED) {
561 return Err(graduated_error(token, None));
562 }
563 Err(revert_error(&revert, &format!("getSwapOutput({token})")))
564 }
565 }
566 }
567
568 async fn buy_fee_bps(&self, network: &str) -> Result<U256, Error> {
578 let mut total = U256::ZERO;
579 for (selector, name) in [
580 (SELECTOR_PLATFORM_BUY_FEE_BPS, "PLATFORM_BUY_FEE_BPS()"),
581 (SELECTOR_TOKEN_OWNER_FEE_BPS, "TOKEN_OWNER_FEE_BPS()"),
582 ] {
583 let part = match self.call_contract(network, selector).await? {
584 CallOutcome::Ok(result) => decode_uint256_at(&result, 0)?,
585 CallOutcome::Reverted(revert) => return Err(revert_error(&revert, name)),
586 };
587 total = total
588 .checked_add(part)
589 .ok_or_else(|| invalid("the contract's buy fees do not fit in a uint256"))?;
590 }
591
592 if total >= U256::from_u64(u64::from(BPS_DIVISOR)) {
593 return Err(invalid(format!(
594 "the contract reports a combined buy fee of {total} bps, which is 100% or more — \
595 refusing to price a buy against it"
596 )));
597 }
598 Ok(total)
599 }
600
601 pub async fn quote_buy(
605 &self,
606 network: &str,
607 token: &str,
608 amount_wei: U256,
609 ) -> Result<BuyQuote, Error> {
610 let token = normalize_address("token", token)?;
611 if amount_wei.is_zero() {
612 return Err(invalid("amount_wei must be greater than zero"));
613 }
614
615 let (amount_out, price_impact_bps) =
616 self.swap_output(network, &token, amount_wei, true).await?;
617 let fee = self.first_buy_fee(network, &token).await?;
618 let value = amount_wei
619 .checked_add(fee)
620 .ok_or_else(|| invalid("amount plus first-buy fee exceeds uint256"))?;
621
622 Ok(BuyQuote {
623 network: network.to_string(),
624 token,
625 amount_in_wei: amount_wei,
626 first_buy_fee_wei: fee,
627 value_wei: value,
628 amount_out_wei: amount_out,
629 price_impact_bps,
630 })
631 }
632
633 pub async fn quote_sell(
635 &self,
636 network: &str,
637 token: &str,
638 amount_wei: U256,
639 ) -> Result<SellQuote, Error> {
640 let token = normalize_address("token", token)?;
641 if amount_wei.is_zero() {
642 return Err(invalid("amount_wei must be greater than zero"));
643 }
644
645 let (amount_out, price_impact_bps) =
646 self.swap_output(network, &token, amount_wei, false).await?;
647
648 Ok(SellQuote {
649 network: network.to_string(),
650 token,
651 amount_in_wei: amount_wei,
652 amount_out_wei: amount_out,
653 price_impact_bps,
654 })
655 }
656
657 pub async fn build_buy(&self, params: &BuyParams) -> Result<BuiltBuy, Error> {
664 let quote = self
665 .quote_buy(¶ms.network, ¶ms.token, params.amount_wei)
666 .await?;
667 let chain = self.resolve(¶ms.network).await?;
668
669 let limit = apply_limit(
670 ¶ms.limits,
671 quote.amount_out_wei,
672 false,
673 "min_amount_out",
674 )?;
675 let deadline = resolve_deadline(¶ms.limits)?;
676
677 Ok(BuiltBuy {
678 transaction: UnsignedTransaction {
679 chain_id: chain.chain_id,
680 to: chain.contract,
681 data: encode_call(
682 SELECTOR_SWAP_EXACT_ETH_FOR_TOKENS,
683 &[
684 address_word("e.token)?,
685 uint256_word(quote.amount_in_wei),
686 uint256_word(limit),
687 uint256_word(U256::from_u64(deadline)),
688 ],
689 ),
690 value: quote.value_wei,
691 from: optional_address(params.limits.from.as_deref())?,
692 },
693 quote,
694 limit_wei: limit,
695 deadline,
696 })
697 }
698
699 pub async fn build_buy_exact_tokens(
725 &self,
726 params: &BuyExactTokensParams,
727 ) -> Result<BuiltBuy, Error> {
728 let token = normalize_address("token", ¶ms.token)?;
729 if params.amount_out_wei.is_zero() {
730 return Err(invalid("amount_out_wei must be greater than zero"));
731 }
732
733 let pool = self.pool_state(¶ms.network, &token).await?;
734 if pool.launched {
735 return Err(graduated_error(&token, None));
736 }
737 if pool.virtual_token_reserve.is_zero() {
738 return Err(invalid(format!(
743 "no bonding curve exists for {token} on {:?} — check the token address and \
744 the network",
745 params.network
746 )));
747 }
748 let fee_bps = self.buy_fee_bps(¶ms.network).await?;
749
750 let deliverable = if params.amount_out_wei > pool.token_reserve {
755 pool.token_reserve
756 } else {
757 params.amount_out_wei
758 };
759 let cost = buy_cost_wei(&pool, deliverable, fee_bps)?;
760
761 let limit = apply_limit(¶ms.limits, cost, true, "max_amount_in")?;
762 let fee = self.first_buy_fee(¶ms.network, &token).await?;
763 let chain = self.resolve(¶ms.network).await?;
764 let deadline = resolve_deadline(¶ms.limits)?;
765 let value = limit
769 .checked_add(fee)
770 .ok_or_else(|| invalid("max input plus first-buy fee exceeds uint256"))?;
771
772 let impact = deliverable
775 .mul_div(
776 U256::from_u64(u64::from(BPS_DIVISOR)),
777 pool.virtual_token_reserve,
778 )
779 .ok_or_else(|| invalid("this token's price impact does not fit in a uint256"))?;
780
781 Ok(BuiltBuy {
782 transaction: UnsignedTransaction {
783 chain_id: chain.chain_id,
784 to: chain.contract,
785 data: encode_call(
786 SELECTOR_SWAP_ETH_FOR_EXACT_TOKENS,
787 &[
788 address_word(&token)?,
789 uint256_word(params.amount_out_wei),
790 uint256_word(limit),
791 uint256_word(U256::from_u64(deadline)),
792 ],
793 ),
794 value,
795 from: optional_address(params.limits.from.as_deref())?,
796 },
797 quote: BuyQuote {
798 network: params.network.clone(),
799 token,
800 amount_in_wei: cost,
801 first_buy_fee_wei: fee,
802 value_wei: value,
803 amount_out_wei: deliverable,
804 price_impact_bps: impact.to_string().parse::<u64>().unwrap_or(u64::MAX),
805 },
806 limit_wei: limit,
807 deadline,
808 })
809 }
810
811 pub async fn build_sell(&self, params: &SellParams) -> Result<BuiltSell, Error> {
817 let quote = self
818 .quote_sell(¶ms.network, ¶ms.token, params.amount_wei)
819 .await?;
820 let chain = self.resolve(¶ms.network).await?;
821
822 let limit = apply_limit(
823 ¶ms.limits,
824 quote.amount_out_wei,
825 false,
826 "min_amount_out",
827 )?;
828 let deadline = resolve_deadline(¶ms.limits)?;
829
830 Ok(BuiltSell {
831 transaction: UnsignedTransaction {
832 chain_id: chain.chain_id,
833 to: chain.contract,
834 data: encode_call(
835 SELECTOR_SWAP_EXACT_TOKENS_FOR_ETH,
836 &[
837 address_word("e.token)?,
838 uint256_word(quote.amount_in_wei),
839 uint256_word(limit),
840 uint256_word(U256::from_u64(deadline)),
841 ],
842 ),
843 value: U256::ZERO,
844 from: optional_address(params.limits.from.as_deref())?,
845 },
846 quote,
847 limit_wei: limit,
848 deadline,
849 })
850 }
851
852 pub async fn build_approve(
858 &self,
859 network: &str,
860 token: &str,
861 amount_wei: U256,
862 ) -> Result<UnsignedTransaction, Error> {
863 let token = normalize_address("token", token)?;
864 let chain = self.resolve(network).await?;
865
866 Ok(UnsignedTransaction {
867 chain_id: chain.chain_id,
868 to: token,
869 data: encode_call(
870 SELECTOR_APPROVE,
871 &[address_word(&chain.contract)?, uint256_word(amount_wei)],
872 ),
873 value: U256::ZERO,
874 from: None,
875 })
876 }
877}
878
879#[derive(Debug, Clone)]
881struct PoolState {
882 token_reserve: U256,
884 virtual_eth_reserve: U256,
886 virtual_token_reserve: U256,
888 launched: bool,
890}
891
892fn buy_cost_wei(pool: &PoolState, amount_out: U256, fee_bps: U256) -> Result<U256, Error> {
899 if amount_out.is_zero() || amount_out >= pool.virtual_token_reserve {
900 return Err(invalid(format!(
904 "the curve cannot deliver {amount_out} tokens: its virtual token reserve is only {}",
905 pool.virtual_token_reserve
906 )));
907 }
908 if pool.virtual_eth_reserve.is_zero() {
909 return Err(invalid(
910 "the curve for this token holds no virtual reserve, so it cannot be priced",
911 ));
912 }
913
914 let denominator = pool
915 .virtual_token_reserve
916 .checked_sub(amount_out)
917 .expect("amount_out is below the virtual token reserve");
918 let before_fee = pool
919 .virtual_eth_reserve
920 .mul_div(amount_out, denominator)
921 .and_then(|floor| floor.checked_add(U256::from_u64(1)))
922 .ok_or_else(|| invalid("pricing this many tokens overflowed uint256"))?;
923
924 let divisor = U256::from_u64(u64::from(BPS_DIVISOR));
925 let net = divisor
926 .checked_sub(fee_bps)
927 .filter(|net| !net.is_zero())
928 .ok_or_else(|| {
929 invalid(format!(
930 "a buy fee of {fee_bps} bps leaves nothing to price"
931 ))
932 })?;
933 before_fee
934 .mul_div(divisor, net)
935 .ok_or_else(|| invalid("grossing this trade up by the buy fee overflowed uint256"))
936}
937
938#[derive(Debug, Clone)]
940struct Resolved {
941 chain_id: i64,
942 contract: String,
943 rpc_url: Option<String>,
944}
945
946fn chain_from(deployment: &Deployment) -> ChainInfo {
953 ChainInfo {
954 name: deployment.name.to_string(),
955 chain_id: deployment.chain_id,
956 network: Some(deployment.network.to_string()),
957 contract_address: Some(deployment.fyuz.to_string()),
958 rpc_url: Some(deployment.rpc_url.to_string()),
959 currency: Some(deployment.currency.to_string()),
960 explorer_url: Some(deployment.explorer_url.to_string()),
961 ..ChainInfo::default()
962 }
963}
964
965#[derive(Debug, Default)]
967pub(crate) struct TradeState {
968 pub(crate) chains: OnceCell<Vec<ChainInfo>>,
969 pub(crate) rpcs: Mutex<HashMap<String, Arc<RpcClient>>>,
970}
971
972fn apply_limit(limits: &Limits, quoted: U256, up: bool, field: &str) -> Result<U256, Error> {
977 if limits.limit_wei.is_some() && limits.slippage_bps.is_some() {
978 return Err(invalid("pass either slippage_bps or limit_wei, not both"));
979 }
980 if let Some(limit) = limits.limit_wei {
981 return Ok(limit);
982 }
983
984 let Some(bps) = limits.slippage_bps else {
985 return Err(invalid(format!(
986 "slippage protection is required: set slippage_bps (100 = 1%) or limit_wei to \
987 choose {field} yourself. There is no default, because the right tolerance \
988 depends on the trade and getting it wrong costs you money"
989 )));
990 };
991 if bps >= BPS_DIVISOR {
992 return Err(invalid(format!(
993 "slippage_bps must be below 10000, got {bps}"
994 )));
995 }
996
997 let factor = if up {
998 BPS_DIVISOR + bps
999 } else {
1000 BPS_DIVISOR - bps
1001 };
1002 let scaled = quoted
1003 .mul_small(factor)
1004 .ok_or_else(|| invalid("applying slippage overflowed uint256"))?;
1005 Ok(scaled.div_rem_small(BPS_DIVISOR).0)
1006}
1007
1008fn resolve_deadline(limits: &Limits) -> Result<u64, Error> {
1010 if let Some(deadline) = limits.deadline {
1011 if deadline == 0 {
1012 return Err(invalid("deadline must be a positive unix timestamp"));
1013 }
1014 return Ok(deadline);
1015 }
1016 let window = limits.deadline_seconds.unwrap_or(DEFAULT_DEADLINE_SECONDS);
1017 if window == 0 {
1018 return Err(invalid("deadline_seconds must be positive"));
1019 }
1020 if window > MAX_DEADLINE_SECONDS {
1021 return Err(invalid(format!(
1022 "deadline_seconds must be at most {MAX_DEADLINE_SECONDS} (one year), got {window}"
1023 )));
1024 }
1025 let now = SystemTime::now()
1026 .duration_since(UNIX_EPOCH)
1027 .unwrap_or(Duration::ZERO)
1028 .as_secs();
1029 Ok(now.saturating_add(window))
1032}
1033
1034fn optional_address(value: Option<&str>) -> Result<Option<String>, Error> {
1035 value.map(|v| normalize_address("from", v)).transpose()
1036}