use crate::client::{ApiError, Transport, info, user_request};
use crate::utils::{Address, Dec, Round};
use serde::Deserialize;
pub const INITIAL_REQUESTS: u64 = 10_000;
pub const THROTTLED_EVERY_MS: u64 = 10_000;
pub const OPEN_ORDERS: u64 = 1_000;
pub const MOST_OPEN_ORDERS: u64 = 5_000;
pub const VOLUME_PER_OPEN_ORDER: i64 = 5_000_000;
pub const READ_EVERY_MS: u64 = 60_000;
pub const READ_BATCH: usize = 10;
#[must_use]
pub fn rate_limit_request(user: Address) -> String {
user_request("userRateLimit", user)
}
pub async fn rate_limit(transport: &impl Transport, user: Address) -> Result<RateLimit, ApiError> {
info(transport, rate_limit_request(user)).await
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize)]
pub struct RateLimit {
#[serde(rename = "cumVlm")]
pub volume: Dec,
#[serde(rename = "nRequestsUsed")]
pub used: u64,
#[serde(rename = "nRequestsCap")]
pub cap: u64,
#[serde(rename = "nRequestsSurplus", default)]
pub surplus: u64,
}
impl RateLimit {
#[must_use]
pub const fn remaining(&self) -> u64 {
self.cap.saturating_sub(self.used)
}
#[must_use]
pub const fn is_throttled(&self) -> bool {
self.remaining() == 0
}
#[must_use]
pub const fn cancel_cap(&self) -> u64 {
let wider = self.cap.saturating_add(100_000);
let doubled = self.cap.saturating_mul(2);
if wider < doubled { wider } else { doubled }
}
#[must_use]
pub fn earned_cap(volume: Dec) -> Option<u64> {
let whole = u64::try_from(volume.units_at(0, Round::Down)?).ok()?;
INITIAL_REQUESTS.checked_add(whole)
}
}
#[must_use]
pub fn volume_for(requests: u64) -> Dec {
Dec::lit::<0>(i128::from(requests))
}
#[must_use]
pub fn surplus_cost(requests: u64) -> Option<Dec> {
Dec::lit::<0>(i128::from(requests)).checked_mul(Dec::lit::<4>(5))
}
#[must_use]
pub fn address_requests(batched: usize) -> u64 {
u64::try_from(batched.max(1)).unwrap_or(u64::MAX)
}
#[must_use]
pub fn open_order_cap(volume: Dec) -> u64 {
let earned = volume
.checked_div(Dec::from_int(VOLUME_PER_OPEN_ORDER), 0, Round::Down)
.and_then(|earned| earned.units_at(0, Round::Down))
.and_then(|earned| u64::try_from(earned).ok())
.unwrap_or(0);
OPEN_ORDERS.saturating_add(earned).min(MOST_OPEN_ORDERS)
}
#[must_use]
pub fn read_wait_ms(read_at: Option<u64>, now: u64) -> Option<u64> {
let next = read_at?.saturating_add(READ_EVERY_MS);
(next > now).then(|| next - now)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Sample {
pub at: u64,
pub limit: RateLimit,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Budget {
pub last: Option<Sample>,
pub base: Option<Sample>,
pub error: Option<String>,
}
impl Budget {
pub fn took(&mut self, sample: Sample) {
if let Some(last) = self.last
&& sample.at.saturating_sub(last.at) >= READ_EVERY_MS
{
self.base = Some(last);
}
self.last = Some(sample);
self.error = None;
}
#[must_use]
pub fn limit(&self) -> Option<RateLimit> {
self.last.map(|sample| sample.limit)
}
fn spent(&self) -> Option<(i128, i128)> {
let (base, last) = (self.base?, self.last?);
let ms = i128::from(last.at) - i128::from(base.at);
if ms <= 0 {
return None;
}
let used = i128::from(last.limit.used) - i128::from(base.limit.used);
let earned = i128::from(last.limit.cap) - i128::from(base.limit.cap);
Some((used - earned, ms))
}
#[must_use]
pub fn net_a_minute(&self) -> Option<i64> {
let (net, ms) = self.spent()?;
i64::try_from(net * 60_000 / ms).ok()
}
#[must_use]
pub fn runs_out_ms(&self) -> Option<u64> {
let (net, ms) = self.spent()?;
if net <= 0 {
return None;
}
let left = i128::from(self.limit()?.remaining());
u64::try_from(left * ms / net).ok()
}
}
#[cfg(test)]
#[path = "tests/limits.rs"]
mod tests;