1use super::{Adapt, Layer, MissingFieldError, RequestParams};
16use crate::{
17 contracts::{Offer, RequestId, Requirements},
18 price_provider::PriceProviderArc,
19 request_builder::ParameterizationMode,
20 selector::{ProofType, SupportedSelectors},
21 util::now_timestamp,
22 LARGE_REQUESTOR_LIST_THRESHOLD_KHZ, XL_REQUESTOR_LIST_THRESHOLD_KHZ,
23};
24use alloy::{
25 network::Ethereum,
26 primitives::{
27 utils::{format_units, Unit},
28 U256,
29 },
30 providers::Provider,
31};
32use anyhow::{Context, Result};
33use clap::Args;
34use derive_builder::Builder;
35
36pub(crate) const DEFAULT_TIMEOUT: u32 = 600;
37pub(crate) const DEFAULT_RAMP_UP_PERIOD: u32 = 60;
38pub(crate) const DEFAULT_MIN_PRICE: U256 = U256::ZERO;
40pub(crate) fn default_max_price_per_cycle() -> U256 {
43 U256::from(100) * Unit::KWEI.wei_const()
44}
45
46pub(crate) fn resolve_min_price(
48 params_min: Option<U256>,
49 config_min_per_cycle: Option<U256>,
50 cycle_count: Option<u64>,
51 market_min: Option<U256>,
52) -> U256 {
53 params_min
54 .or_else(|| cycle_count.and_then(|c| config_min_per_cycle.map(|p| p * U256::from(c))))
55 .or(market_min)
56 .unwrap_or(DEFAULT_MIN_PRICE)
57}
58
59pub(crate) fn resolve_max_price(
61 params_max: Option<U256>,
62 config_max: Option<U256>,
63 market_max: Option<U256>,
64 cycle_count: Option<u64>,
65) -> U256 {
66 params_max.or(config_max).or(market_max).unwrap_or_else(|| {
67 let n = if let Some(count) = cycle_count {
69 count.max(1)
70 } else {
71 tracing::warn!("No cycle count provided using static fallback, defaulting to 1");
72 1
73 };
74 default_max_price_per_cycle() * U256::from(n)
75 })
76}
77
78struct CollateralRecommendation {
79 default: U256,
80 large: U256,
81 xl: U256,
82}
83
84impl CollateralRecommendation {
85 fn new(default: U256, large: U256, xl: U256) -> Self {
86 Self { default, large, xl }
87 }
88
89 fn recommend_collateral(
93 &self,
94 secondary_performance: f64,
95 lock_collateral: U256,
96 ) -> anyhow::Result<Option<U256>> {
97 let recommended = if secondary_performance < LARGE_REQUESTOR_LIST_THRESHOLD_KHZ
98 && lock_collateral < self.default
99 {
100 Some(self.default)
101 } else if (LARGE_REQUESTOR_LIST_THRESHOLD_KHZ..XL_REQUESTOR_LIST_THRESHOLD_KHZ)
102 .contains(&secondary_performance)
103 && lock_collateral < self.large
104 {
105 Some(self.large)
106 } else if secondary_performance >= XL_REQUESTOR_LIST_THRESHOLD_KHZ
107 && lock_collateral < self.xl
108 {
109 Some(self.xl)
110 } else {
111 None
112 };
113 Ok(recommended)
114 }
115}
116fn check_primary_performance_warning(cycle_count: u64, primary_performance: f64) -> bool {
120 if primary_performance > XL_REQUESTOR_LIST_THRESHOLD_KHZ {
121 let recommended_lock_timeout =
122 cycle_count.div_ceil(XL_REQUESTOR_LIST_THRESHOLD_KHZ as u64) as u32;
123 tracing::warn!(
124 "Warning: your request requires a proving Khz of {primary_performance} to be \
125 fulfilled before the lock timeout. This limits the number of provers in the \
126 network that will be able to fulfill your order. Consider setting a longer \
127 lock timeout of at least {recommended_lock_timeout} seconds."
128 );
129 true
130 } else {
131 false
132 }
133}
134
135fn check_secondary_performance_warning(
139 cycle_count: u64,
140 secondary_performance: f64,
141 lock_timeout: u32,
142) -> bool {
143 if secondary_performance > XL_REQUESTOR_LIST_THRESHOLD_KHZ {
144 let min_secondary_window =
147 cycle_count.div_ceil(XL_REQUESTOR_LIST_THRESHOLD_KHZ as u64) as u32;
148 let recommended_timeout = lock_timeout.saturating_add(min_secondary_window);
150 tracing::warn!(
151 "Warning: your request requires a proving Khz of {secondary_performance} to be \
152 fulfilled before the timeout. This limits the number of provers in the network \
153 that will be able to fulfill your order. Consider setting a longer timeout of \
154 at least {recommended_timeout} seconds."
155 );
156 true
157 } else {
158 false
159 }
160}
161
162#[non_exhaustive]
167#[derive(Clone, Builder)]
168pub struct OfferLayerConfig {
169 #[builder(setter(into), default = "Some(ParameterizationMode::fulfillment())")]
182 pub parameterization_mode: Option<ParameterizationMode>,
183
184 #[builder(setter(into, strip_option), default)]
186 pub min_price_per_cycle: Option<U256>,
187
188 #[builder(setter(into, strip_option), default)]
190 pub max_price_per_cycle: Option<U256>,
191
192 #[builder(setter(strip_option), default)]
194 pub bidding_start_delay: Option<u64>,
195
196 #[builder(setter(strip_option), default)]
198 pub ramp_up_period: Option<u32>,
199
200 #[builder(setter(strip_option), default)]
202 pub lock_timeout: Option<u32>,
203
204 #[builder(setter(strip_option), default)]
206 pub timeout: Option<u32>,
207
208 #[builder(setter(strip_option, into), default)]
210 pub lock_collateral: Option<U256>,
211
212 #[builder(default = "200_000")]
214 pub lock_gas_estimate: u64,
215
216 #[builder(default = "750_000")]
218 pub fulfill_gas_estimate: u64,
219
220 #[builder(default = "250_000")]
222 pub groth16_verify_gas_estimate: u64,
223
224 #[builder(default = "100_000")]
226 pub smart_contract_sig_verify_gas_estimate: u64,
227
228 #[builder(setter(into), default)]
230 pub supported_selectors: SupportedSelectors,
231}
232
233#[non_exhaustive]
234pub struct OfferLayer<P> {
243 pub provider: P,
245
246 pub config: OfferLayerConfig,
248
249 pub price_provider: Option<PriceProviderArc>,
252}
253
254impl<P: Clone> Clone for OfferLayer<P> {
255 fn clone(&self) -> Self {
256 Self {
257 provider: self.provider.clone(),
258 config: self.config.clone(),
259 price_provider: self.price_provider.clone(),
260 }
261 }
262}
263
264impl OfferLayerConfig {
265 pub fn builder() -> OfferLayerConfigBuilder {
270 Default::default()
271 }
272}
273
274impl Default for OfferLayerConfig {
275 fn default() -> Self {
276 Self::builder().build().expect("implementation error in Default for OfferLayerConfig")
277 }
278}
279
280impl<P: Clone> From<P> for OfferLayer<P> {
281 fn from(provider: P) -> Self {
282 OfferLayer { provider, config: Default::default(), price_provider: None }
283 }
284}
285
286#[non_exhaustive]
287#[derive(Clone, Debug, Default, Builder, Args)]
288pub struct OfferParams {
291 #[clap(long)]
293 #[builder(setter(strip_option, into), default)]
294 pub min_price: Option<U256>,
295
296 #[clap(long)]
298 #[builder(setter(strip_option, into), default)]
299 pub max_price: Option<U256>,
300
301 #[clap(long)]
303 #[builder(setter(strip_option), default)]
304 pub bidding_start: Option<u64>,
305
306 #[clap(long)]
308 #[builder(setter(strip_option), default)]
309 pub ramp_up_period: Option<u32>,
310
311 #[clap(long)]
313 #[builder(setter(strip_option), default)]
314 pub lock_timeout: Option<u32>,
315
316 #[clap(long)]
318 #[builder(setter(strip_option), default)]
319 pub timeout: Option<u32>,
320
321 #[clap(long)]
323 #[builder(setter(strip_option, into), default)]
324 pub lock_collateral: Option<U256>,
325}
326
327impl From<Offer> for OfferParams {
328 fn from(value: Offer) -> Self {
329 Self {
330 timeout: Some(value.timeout),
331 min_price: Some(value.minPrice),
332 max_price: Some(value.maxPrice),
333 lock_collateral: Some(value.lockCollateral),
334 lock_timeout: Some(value.lockTimeout),
335 bidding_start: Some(value.rampUpStart),
336 ramp_up_period: Some(value.rampUpPeriod),
337 }
338 }
339}
340
341impl From<OfferParamsBuilder> for OfferParams {
342 fn from(value: OfferParamsBuilder) -> Self {
343 value.build().expect("implementation error in OfferParams")
345 }
346}
347
348impl From<&mut OfferParamsBuilder> for OfferParams {
350 fn from(value: &mut OfferParamsBuilder) -> Self {
351 value.clone().into()
352 }
353}
354
355impl TryFrom<OfferParams> for Offer {
356 type Error = MissingFieldError;
357
358 fn try_from(value: OfferParams) -> Result<Self, Self::Error> {
359 Ok(Self {
360 timeout: value.timeout.ok_or(MissingFieldError::new("timeout"))?,
361 minPrice: value.min_price.ok_or(MissingFieldError::new("min_price"))?,
362 maxPrice: value.max_price.ok_or(MissingFieldError::new("max_price"))?,
363 lockCollateral: value
364 .lock_collateral
365 .ok_or(MissingFieldError::new("lock_collateral"))?,
366 lockTimeout: value.lock_timeout.ok_or(MissingFieldError::new("lock_timeout"))?,
367 rampUpStart: value.bidding_start.ok_or(MissingFieldError::new("bidding_start"))?,
368 rampUpPeriod: value.ramp_up_period.ok_or(MissingFieldError::new("ramp_up_period"))?,
369 })
370 }
371}
372
373impl OfferParams {
374 pub fn builder() -> OfferParamsBuilder {
379 Default::default()
380 }
381}
382
383impl<P> OfferLayer<P>
384where
385 P: Provider<Ethereum> + 'static + Clone,
386{
387 pub fn new(provider: P, config: OfferLayerConfig) -> Self {
392 Self { provider, config, price_provider: None }
393 }
394
395 pub fn with_price_provider(self, price_provider: Option<PriceProviderArc>) -> Self {
408 Self { price_provider, ..self }
409 }
410
411 pub fn estimate_gas_usage_upper_bound(
419 &self,
420 requirements: &Requirements,
421 request_id: &RequestId,
422 ) -> anyhow::Result<u64> {
423 let mut gas_usage_estimate =
424 self.config.lock_gas_estimate + self.config.fulfill_gas_estimate;
425 if request_id.smart_contract_signed {
426 gas_usage_estimate += self.config.smart_contract_sig_verify_gas_estimate;
427 }
428 if let Some(callback) = requirements.callback.as_option() {
429 gas_usage_estimate +=
430 u64::try_from(callback.gasLimit).context("callback gas limit too large for u64")?;
431 }
432
433 let proof_type = self
434 .config
435 .supported_selectors
436 .proof_type(requirements.selector)
437 .context("cannot estimate gas usage for request with unsupported selector")?;
438 if let ProofType::Groth16 = proof_type {
439 gas_usage_estimate += self.config.groth16_verify_gas_estimate;
440 };
441 Ok(gas_usage_estimate)
442 }
443
444 pub fn estimate_gas_cost_upper_bound(
452 &self,
453 requirements: &Requirements,
454 request_id: &RequestId,
455 gas_price: u128,
456 ) -> anyhow::Result<U256> {
457 let gas_usage_estimate = self.estimate_gas_usage_upper_bound(requirements, request_id)?;
458
459 let gas_cost_estimate = gas_price * (gas_usage_estimate as u128);
460 Ok(U256::from(gas_cost_estimate))
461 }
462
463 pub async fn max_price_with_gas(
468 &self,
469 requirements: &Requirements,
470 request_id: &RequestId,
471 max_price: U256,
472 ) -> anyhow::Result<U256> {
473 let gas_price: u128 = self.provider.get_gas_price().await?;
474 let gas_cost_estimate =
475 self.estimate_gas_cost_upper_bound(requirements, request_id, gas_price)?;
476 let adjusted_gas_cost_estimate = gas_cost_estimate + gas_cost_estimate;
477 let adjusted_max_price = max_price + adjusted_gas_cost_estimate;
478 tracing::debug!(
479 "Setting a max price of {} ether: {} max_price + {} (2x) gas_cost_estimate [gas price: {} gwei]",
480 format_units(adjusted_max_price, "ether")?,
481 format_units(max_price, "ether")?,
482 format_units(adjusted_gas_cost_estimate, "ether")?,
483 format_units(U256::from(gas_price), "gwei")?,
484 );
485 Ok(adjusted_max_price)
486 }
487}
488
489impl<P> Layer<(&Requirements, &RequestId, Option<u64>, &OfferParams)> for OfferLayer<P>
490where
491 P: Provider<Ethereum> + 'static + Clone,
492{
493 type Output = Offer;
494 type Error = anyhow::Error;
495
496 async fn process(
497 &self,
498 (requirements, request_id, cycle_count, params): (
499 &Requirements,
500 &RequestId,
501 Option<u64>,
502 &OfferParams,
503 ),
504 ) -> Result<Self::Output, Self::Error> {
505 let (market_min_price, market_max_price) = if (params.min_price.is_none()
507 && self.config.min_price_per_cycle.is_none())
508 || (params.max_price.is_none() && self.config.max_price_per_cycle.is_none())
509 {
510 if let Some(ref price_provider) = self.price_provider {
511 if let Some(cycle_count) = cycle_count {
512 match price_provider.price_percentiles().await {
513 Ok(percentiles) => {
514 let min = U256::ZERO;
515 let max = percentiles.p99.min(percentiles.p50 * U256::from(2))
516 * U256::from(cycle_count);
517 tracing::debug!(
518 "Using market prices from price provider: min={}, max={} (for {} cycles)",
519 format_units(min, "ether")?,
520 format_units(max, "ether")?,
521 cycle_count
522 );
523 (Some(min), Some(max))
524 }
525 Err(e) => {
526 tracing::warn!(
527 "Failed to fetch market prices from price provider: {}. Falling back to config-based pricing.",
528 e
529 );
530 (None, None)
531 }
532 }
533 } else {
534 tracing::warn!("No cycle count provided, falling back to default pricing");
535 (None, None)
536 }
537 } else {
538 (None, None)
539 }
540 } else {
541 (None, None)
542 };
543
544 let min_price = resolve_min_price(
546 params.min_price,
547 self.config.min_price_per_cycle,
548 cycle_count,
549 market_min_price,
550 );
551
552 let config_max_value = if params.max_price.is_none() {
553 let c = cycle_count.context("cycle count required to set max price in OfferLayer")?;
554 if let Some(per_cycle) = self.config.max_price_per_cycle {
555 let max_price = per_cycle * U256::from(c);
556 Some(max_price)
557 } else {
558 None
559 }
560 } else {
561 None
562 };
563
564 let max_price =
565 resolve_max_price(params.max_price, config_max_value, market_max_price, cycle_count);
566 let max_price = if params.max_price.is_none() {
567 self.max_price_with_gas(requirements, request_id, max_price).await?
568 } else {
569 max_price
570 };
571
572 let (recommended_ramp_up_start, recommended_ramp_up_period, recommended_lock_timeout) =
574 match self.config.parameterization_mode {
575 Some(m) => (
576 Some(m.recommended_ramp_up_start(cycle_count)),
577 Some(m.recommended_ramp_up_period(cycle_count)),
578 Some(m.recommended_timeout(cycle_count)),
579 ),
580 None => (None, None, None),
581 };
582
583 let ramp_up_start = self
584 .config
585 .bidding_start_delay
586 .map(|d| now_timestamp() + d)
587 .or(recommended_ramp_up_start)
588 .unwrap_or_else(|| now_timestamp() + 15);
589
590 let ramp_up_period = self
591 .config
592 .ramp_up_period
593 .or(recommended_ramp_up_period)
594 .unwrap_or(DEFAULT_RAMP_UP_PERIOD);
595
596 let lock_timeout = self
597 .config
598 .lock_timeout
599 .or(recommended_lock_timeout)
600 .unwrap_or(DEFAULT_TIMEOUT + DEFAULT_RAMP_UP_PERIOD);
601
602 let timeout = self
603 .config
604 .timeout
605 .or(Some(lock_timeout * 2))
606 .unwrap_or((DEFAULT_TIMEOUT + DEFAULT_RAMP_UP_PERIOD) * 2);
607
608 if self.config.bidding_start_delay.is_none() && recommended_ramp_up_start.is_none() {
609 tracing::warn!("Using default ramp up start: {}", ramp_up_start);
610 }
611 if self.config.ramp_up_period.is_none() && recommended_ramp_up_period.is_none() {
612 tracing::warn!("Using default ramp up period: {}", ramp_up_period);
613 }
614 if self.config.lock_timeout.is_none() && recommended_lock_timeout.is_none() {
615 tracing::warn!("Using default lock timeout: {}", lock_timeout);
616 }
617 if self.config.timeout.is_none() && recommended_lock_timeout.is_none() {
618 tracing::warn!("Using default timeout: {}", timeout);
619 }
620
621 let chain_id = self.provider.get_chain_id().await?;
622 let default_collaterals = default_lock_collateral(chain_id);
623 let lock_collateral = self.config.lock_collateral.unwrap_or(default_collaterals.default);
624
625 let offer = Offer {
626 minPrice: min_price,
627 maxPrice: max_price,
628 rampUpStart: params.bidding_start.unwrap_or(ramp_up_start),
629 rampUpPeriod: params.ramp_up_period.unwrap_or(ramp_up_period),
630 lockTimeout: params.lock_timeout.unwrap_or(lock_timeout),
631 timeout: params.timeout.unwrap_or(timeout),
632 lockCollateral: params.lock_collateral.unwrap_or(lock_collateral),
633 };
634
635 if let Some(cycle_count) = cycle_count {
636 let primary_performance = offer.required_khz_performance(cycle_count);
637 let secondary_performance =
638 offer.required_khz_performance_secondary_prover(cycle_count);
639
640 check_primary_performance_warning(cycle_count, primary_performance);
641 check_secondary_performance_warning(
642 cycle_count,
643 secondary_performance,
644 offer.lockTimeout,
645 );
646
647 if let Some(collateral) = default_collaterals
649 .recommend_collateral(secondary_performance, offer.lockCollateral)?
650 {
651 tracing::warn!(
652 "Warning: the collateral requirement of your request is low. This means the \
653 incentives for secondary provers to fulfill the order if the primary prover \
654 is slashed may be too low. It is recommended to set the lock collateral to at least {} ZKC.",
655 format_units(collateral, "ether")?
656 );
657 }
658 }
659
660 Ok(offer)
661 }
662}
663
664fn default_lock_collateral(chain_id: u64) -> CollateralRecommendation {
666 match chain_id {
667 8453 => CollateralRecommendation::new(
668 U256::from(20) * Unit::ETHER.wei_const(),
669 U256::from(50) * Unit::ETHER.wei_const(),
670 U256::from(100) * Unit::ETHER.wei_const(),
671 ), 84532 => CollateralRecommendation::new(
673 U256::from(5) * Unit::ETHER.wei_const(),
674 U256::from(10) * Unit::ETHER.wei_const(),
675 U256::from(20) * Unit::ETHER.wei_const(),
676 ), 11155111 => CollateralRecommendation::new(
678 U256::from(5) * Unit::ETHER.wei_const(),
679 U256::from(10) * Unit::ETHER.wei_const(),
680 U256::from(20) * Unit::ETHER.wei_const(),
681 ), _ => CollateralRecommendation::new(
684 U256::from(5) * Unit::ETHER.wei_const(),
685 U256::from(10) * Unit::ETHER.wei_const(),
686 U256::from(20) * Unit::ETHER.wei_const(),
687 ),
688 }
689}
690
691impl<P> Adapt<OfferLayer<P>> for RequestParams
692where
693 P: Provider<Ethereum> + 'static + Clone,
694{
695 type Output = RequestParams;
696 type Error = anyhow::Error;
697
698 async fn process_with(self, layer: &OfferLayer<P>) -> Result<Self::Output, Self::Error> {
699 tracing::trace!("Processing {self:?} with OfferLayer");
700
701 let requirements: Requirements = self
702 .requirements
703 .clone()
704 .try_into()
705 .context("failed to construct requirements in OfferLayer")?;
706 let request_id = self.require_request_id()?;
707
708 let offer = layer.process((&requirements, request_id, self.cycles, &self.offer)).await?;
709 Ok(self.with_offer(offer))
710 }
711}
712
713#[cfg(test)]
714mod tests {
715 use super::*;
716 use alloy::primitives::utils::parse_ether;
717 use tracing_test::traced_test;
718
719 fn default_collaterals() -> CollateralRecommendation {
720 CollateralRecommendation::new(
721 parse_ether("20").unwrap(),
722 parse_ether("50").unwrap(),
723 parse_ether("100").unwrap(),
724 )
725 }
726
727 mod performance_warnings {
728 use super::*;
729
730 #[test]
731 fn primary_below_threshold() {
732 assert!(!check_primary_performance_warning(1000, 5000.0));
733 }
734
735 #[test]
736 #[traced_test]
737 fn primary_above_threshold() {
738 assert!(check_primary_performance_warning(20000, 15000.0));
739 assert!(logs_contain("Warning: your request requires a proving Khz"));
740 assert!(logs_contain("lock timeout"));
741 }
742
743 #[test]
744 fn primary_at_threshold() {
745 assert!(!check_primary_performance_warning(1000, XL_REQUESTOR_LIST_THRESHOLD_KHZ));
746 }
747
748 #[test]
749 fn secondary_below_threshold() {
750 assert!(!check_secondary_performance_warning(1000, 5000.0, 600));
751 }
752
753 #[test]
754 #[traced_test]
755 fn secondary_above_threshold() {
756 assert!(check_secondary_performance_warning(20000, 15000.0, 600));
757 assert!(logs_contain("Warning: your request requires a proving Khz"));
758 assert!(logs_contain("timeout"));
759 }
760
761 #[test]
762 fn secondary_at_threshold() {
763 assert!(!check_secondary_performance_warning(
764 1000,
765 XL_REQUESTOR_LIST_THRESHOLD_KHZ,
766 600
767 ));
768 }
769 }
770
771 mod collateral_recommendations {
772 use super::*;
773
774 #[test]
775 fn low_performance_low_collateral() {
776 let result = default_collaterals()
777 .recommend_collateral(2000.0, parse_ether("10").unwrap())
778 .unwrap();
779 assert_eq!(result, Some(parse_ether("20").unwrap()));
780 }
781
782 #[test]
783 fn low_performance_sufficient_collateral() {
784 let result = default_collaterals()
785 .recommend_collateral(2000.0, parse_ether("25").unwrap())
786 .unwrap();
787 assert_eq!(result, None);
788 }
789
790 #[test]
791 fn medium_performance_low_collateral() {
792 let result = default_collaterals()
793 .recommend_collateral(5000.0, parse_ether("30").unwrap())
794 .unwrap();
795 assert_eq!(result, Some(parse_ether("50").unwrap()));
796 }
797
798 #[test]
799 fn medium_performance_sufficient_collateral() {
800 let result = default_collaterals()
801 .recommend_collateral(5000.0, parse_ether("60").unwrap())
802 .unwrap();
803 assert_eq!(result, None);
804 }
805
806 #[test]
807 fn high_performance_low_collateral() {
808 let result = default_collaterals()
809 .recommend_collateral(12000.0, parse_ether("80").unwrap())
810 .unwrap();
811 assert_eq!(result, Some(parse_ether("100").unwrap()));
812 }
813
814 #[test]
815 fn high_performance_sufficient_collateral() {
816 let result = default_collaterals()
817 .recommend_collateral(12000.0, parse_ether("120").unwrap())
818 .unwrap();
819 assert_eq!(result, None);
820 }
821
822 #[test]
823 fn at_large_threshold() {
824 let result = default_collaterals()
825 .recommend_collateral(
826 LARGE_REQUESTOR_LIST_THRESHOLD_KHZ,
827 parse_ether("10").unwrap(),
828 )
829 .unwrap();
830 assert_eq!(result, Some(parse_ether("50").unwrap()));
831 }
832
833 #[test]
834 fn at_xl_threshold() {
835 let result = default_collaterals()
836 .recommend_collateral(XL_REQUESTOR_LIST_THRESHOLD_KHZ, parse_ether("80").unwrap())
837 .unwrap();
838 assert_eq!(result, Some(parse_ether("100").unwrap()));
839 }
840
841 #[test]
842 fn exact_threshold_low() {
843 let result = default_collaterals()
844 .recommend_collateral(2000.0, parse_ether("20").unwrap())
845 .unwrap();
846 assert_eq!(result, None);
847 }
848
849 #[test]
850 fn exact_threshold_medium() {
851 let result = default_collaterals()
852 .recommend_collateral(5000.0, parse_ether("50").unwrap())
853 .unwrap();
854 assert_eq!(result, None);
855 }
856
857 #[test]
858 fn exact_threshold_high() {
859 let result = default_collaterals()
860 .recommend_collateral(12000.0, parse_ether("100").unwrap())
861 .unwrap();
862 assert_eq!(result, None);
863 }
864 }
865
866 mod price_priority {
867 use super::*;
868
869 fn u(n: u64) -> U256 {
870 U256::from(n)
871 }
872
873 #[test]
874 fn min_price_params_takes_priority() {
875 assert_eq!(resolve_min_price(Some(u(1)), Some(u(2)), Some(10), Some(u(3))), u(1));
876 }
877
878 #[test]
879 fn min_price_config_over_market_and_default() {
880 assert_eq!(resolve_min_price(None, Some(u(5)), Some(10), Some(u(100))), u(50));
881 }
882
883 #[test]
884 fn min_price_market_over_default() {
885 assert_eq!(resolve_min_price(None, None, None, Some(u(42))), u(42));
886 }
887
888 #[test]
889 fn min_price_default_when_all_none() {
890 assert_eq!(
891 resolve_min_price(None, None, None, None),
892 DEFAULT_MIN_PRICE * U256::from(1)
893 );
894 }
895
896 #[test]
897 fn min_price_config_requires_cycle_count() {
898 assert_eq!(resolve_min_price(None, Some(u(5)), None, Some(u(10))), u(10));
899 }
900
901 #[test]
902 fn max_price_params_takes_priority() {
903 assert_eq!(resolve_max_price(Some(u(1)), Some(u(2)), Some(u(3)), Some(10)), u(1));
904 }
905
906 #[test]
907 fn max_price_config_over_market_and_default() {
908 assert_eq!(resolve_max_price(None, Some(u(50)), Some(u(100)), Some(10)), u(50));
909 }
910
911 #[test]
912 fn max_price_market_over_default() {
913 assert_eq!(resolve_max_price(None, None, Some(u(42)), Some(10)), u(42));
914 }
915
916 #[test]
917 fn max_price_default_when_all_none() {
918 assert_eq!(
919 resolve_max_price(None, None, None, Some(10)),
920 default_max_price_per_cycle() * u(10)
921 );
922 }
923 }
924}