use std::collections::VecDeque;
use crate::client::network::Network;
pub const WINDOW_MS: u64 = 60_000;
pub const BASE_COOLDOWN_MS: u64 = 2_000;
pub const MAX_COOLDOWN_MS: u64 = 60_000;
const MAX_STRIKES: u32 = 6;
pub const STANDARD_FLOOR: u32 = 400;
pub const STANDARD_BUDGET: u32 = 24_000;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum Tier {
#[default]
Standard,
Plus,
Premium,
Builder,
}
impl Tier {
#[must_use]
pub const fn budget(self, network: Network) -> u32 {
match (self, network) {
(Tier::Standard, _) => STANDARD_BUDGET,
(Tier::Plus, Network::Lighter) | (Tier::Premium, _) => 24_000,
(Tier::Plus, Network::Robinhood) => 120_000,
(Tier::Builder, Network::Lighter) => 240_000,
(Tier::Builder, Network::Robinhood) => 150_000,
}
}
#[must_use]
pub const fn cost(self, weight: u32) -> u32 {
match self {
Tier::Standard if weight < STANDARD_FLOOR => STANDARD_FLOOR,
_ => weight,
}
}
#[must_use]
pub const fn sends_share_reads(self) -> bool {
matches!(self, Tier::Standard)
}
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Tier::Standard => "standard",
Tier::Plus => "plus",
Tier::Premium => "premium",
Tier::Builder => "builder",
}
}
}
#[must_use]
pub fn weight(network: Network, path: &str) -> u32 {
match (path, network) {
("sendTx" | "sendTxBatch" | "nextNonce", _) => 6,
("tv/history" | "tv/streaming", _) => 10,
("publicPools" | "publicPoolsMetadata" | "txFromL1TxHash", _) => 50,
("accountInactiveOrders" | "accountActiveOrders" | "accountOrders" | "deposit/latest", _) => 100,
("exchangeMetrics" | "executeStats", _) => 120,
("apikeys", _) | ("account", Network::Robinhood) => 150,
("trades", Network::Lighter) => 200,
("transferFeeInfo", _) => 500,
("recentTrades", _) | ("trades", Network::Robinhood) => 600,
(
"changeAccountTier"
| "tokens/revoke"
| "setAccountMetadata"
| "notification/ack"
| "createIntentAddress"
| "fastwithdraw"
| "referral/create"
| "referral/use"
| "referral/update"
| "referral/kickback/update",
_,
) => 3_000,
("tokens/create", Network::Lighter) => 10_000,
("tokens/create", Network::Robinhood) => 23_000,
_ => 300,
}
}
#[must_use]
pub fn is_send(path: &str) -> bool {
matches!(path, "sendTx" | "sendTxBatch" | "fastwithdraw" | "litLease")
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Spent {
pub budget: u32,
pub minute: u32,
pub kinds: Vec<Kind>,
pub refused: u32,
pub cooldown_ms: u64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Kind {
pub name: String,
pub count: u32,
pub weight: u32,
}
#[derive(Debug, Default)]
pub(crate) struct Meter {
sent: VecDeque<(u64, String, u32)>,
refused: VecDeque<u64>,
}
impl Meter {
fn prune(&mut self, now: u64) {
let gone = |at: u64| at.saturating_add(WINDOW_MS) <= now;
while self.sent.front().is_some_and(|(at, _, _)| gone(*at)) {
self.sent.pop_front();
}
while self.refused.front().is_some_and(|at| gone(*at)) {
self.refused.pop_front();
}
}
pub(crate) fn record(&mut self, now: u64, kind: &str, cost: u32) {
self.prune(now);
self.sent.push_back((now, kind.to_owned(), cost));
}
pub(crate) fn refuse(&mut self, now: u64) {
self.prune(now);
self.refused.push_back(now);
}
pub(crate) fn wait_for(&mut self, now: u64, need: u32, budget: u32) -> Option<u64> {
self.prune(now);
let soft = budget / 5 * 4;
let spent = self.sent.iter().fold(0u32, |sum, (_, _, cost)| sum.saturating_add(*cost));
let mut over = spent.saturating_add(need).checked_sub(soft).filter(|over| *over > 0)?;
for (at, _, cost) in &self.sent {
if *cost >= over {
return Some(at.saturating_add(WINDOW_MS).saturating_sub(now).max(1));
}
over -= cost;
}
Some(WINDOW_MS)
}
pub(crate) fn spent(&mut self, now: u64, budget: u32) -> Spent {
self.prune(now);
let mut kinds: Vec<Kind> = Vec::new();
for (_, name, cost) in &self.sent {
match kinds.iter_mut().find(|kind| kind.name == *name) {
Some(kind) => {
kind.count += 1;
kind.weight = kind.weight.saturating_add(*cost);
}
None => kinds.push(Kind { name: name.clone(), count: 1, weight: *cost }),
}
}
kinds.sort_by(|a, b| b.weight.cmp(&a.weight).then_with(|| a.name.cmp(&b.name)));
let minute = kinds.iter().fold(0u32, |sum, kind| sum.saturating_add(kind.weight));
Spent { budget, minute, kinds, refused: u32::try_from(self.refused.len()).unwrap_or(u32::MAX), cooldown_ms: 0 }
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct Ticket(u64);
#[derive(Debug, Default)]
pub(crate) struct Gate {
until: u64,
strikes: u32,
epoch: u64,
}
impl Gate {
pub(crate) fn open(&self, now: u64) -> Result<Ticket, u64> {
match self.until > now {
true => Err(self.until - now),
false => Ok(Ticket(self.epoch)),
}
}
pub(crate) fn passed(&mut self, ticket: Ticket) {
if ticket.0 == self.epoch {
self.strikes = 0;
}
}
pub(crate) fn struck(&mut self, ticket: Ticket, now: u64, retry_after_ms: Option<u64>) -> u64 {
let wait = match ticket.0 == self.epoch {
true => {
self.epoch += 1;
self.strikes = (self.strikes + 1).min(MAX_STRIKES);
let backoff = (BASE_COOLDOWN_MS << (self.strikes - 1)).min(MAX_COOLDOWN_MS);
backoff.max(retry_after_ms.unwrap_or(0))
}
false => retry_after_ms.unwrap_or(0),
};
self.until = self.until.max(now.saturating_add(wait));
self.until.saturating_sub(now)
}
pub(crate) fn blocked(&mut self, ticket: Ticket, now: u64) -> u64 {
self.struck(ticket, now, Some(MAX_COOLDOWN_MS))
}
#[must_use]
pub(crate) const fn cooldown_ms(&self, now: u64) -> u64 {
self.until.saturating_sub(now)
}
}
#[cfg(test)]
#[path = "tests/limit.rs"]
mod tests;