use crate::account::Signers;
use crate::client::Network;
use crate::earn::staking::actions::{CDeposit, CWithdraw, TokenDelegate};
use crate::earn::staking::reads::{Delegation, DelegatorSummary, MAX_PENDING_WITHDRAWALS, locked_until, unstaking_ms};
use crate::utils::{Address, BadUnits, Dec};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Balance {
Undelegated,
Delegated(Address),
Pending,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Change {
Up(Dec),
Down(Dec),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Effect {
pub balance: Balance,
pub change: Change,
}
impl Effect {
#[must_use]
pub fn landed(&self, before: Dec, after: Dec) -> bool {
match self.change {
Change::Up(amount) => before.checked_add(amount) == Some(after),
Change::Down(amount) => before.checked_sub(amount) == Some(after),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Plan<A> {
pub network: Network,
pub action: A,
pub nonce: u64,
pub key: Signers,
pub effect: Effect,
pub free_at: Option<u64>,
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum Refused {
#[error("{amount} HYPE is more than the {held} there is")]
MoreThan { amount: Dec, held: Dec },
#[error("nothing is delegated to {0}")]
NoDelegation(Address),
#[error("that delegation is locked until {until} ms")]
Locked { until: u64 },
#[error("{MAX_PENDING_WITHDRAWALS} withdrawals already wait in the queue, the most there may be")]
QueueFull,
#[error(transparent)]
Units(#[from] BadUnits),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Ctx {
pub network: Network,
pub nonce: u64,
pub now_ms: u64,
}
impl Ctx {
const fn plan<A>(&self, action: A, key: Signers, effect: Effect, free_at: Option<u64>) -> Plan<A> {
Plan { network: self.network, action, nonce: self.nonce, key, effect, free_at }
}
pub fn stake(&self, hype: Dec) -> Result<Plan<CDeposit>, Refused> {
let action = CDeposit::new(hype, self.nonce)?;
let effect = Effect { balance: Balance::Undelegated, change: Change::Up(hype) };
Ok(self.plan(action, CDeposit::KEY, effect, None))
}
pub fn unstake(&self, hype: Dec, summary: &DelegatorSummary) -> Result<Plan<CWithdraw>, Refused> {
let action = CWithdraw::new(hype, self.nonce)?;
at_most(hype, summary.undelegated)?;
if summary.n_pending_withdrawals >= MAX_PENDING_WITHDRAWALS {
return Err(Refused::QueueFull);
}
let effect = Effect { balance: Balance::Undelegated, change: Change::Down(hype) };
let ready = self.now_ms.saturating_add(unstaking_ms(self.network));
Ok(self.plan(action, CWithdraw::KEY, effect, Some(ready)))
}
pub fn delegate(
&self,
validator: Address,
hype: Dec,
summary: &DelegatorSummary,
) -> Result<Plan<TokenDelegate>, Refused> {
let action = TokenDelegate::delegate(validator, hype, self.nonce)?;
at_most(hype, summary.undelegated)?;
let effect = Effect { balance: Balance::Undelegated, change: Change::Down(hype) };
Ok(self.plan(action, TokenDelegate::KEY, effect, Some(locked_until(self.network, self.now_ms))))
}
pub fn undelegate(
&self,
validator: Address,
hype: Dec,
delegations: &[Delegation],
) -> Result<Plan<TokenDelegate>, Refused> {
let action = TokenDelegate::undelegate(validator, hype, self.nonce)?;
let held = delegations
.iter()
.find(|delegation| delegation.validator == validator)
.ok_or(Refused::NoDelegation(validator))?;
if held.is_locked(self.now_ms) {
return Err(Refused::Locked { until: held.locked_until_timestamp });
}
at_most(hype, held.amount)?;
let effect = Effect { balance: Balance::Undelegated, change: Change::Up(hype) };
Ok(self.plan(action, TokenDelegate::KEY, effect, None))
}
}
fn at_most(amount: Dec, held: Dec) -> Result<(), Refused> {
match amount > held {
true => Err(Refused::MoreThan { amount, held }),
false => Ok(()),
}
}
#[cfg(test)]
#[path = "tests/plan.rs"]
mod tests;