use std::{
future::{Future, ready},
pin::pin,
task::{Context, Poll, Waker},
};
use crate::client::{Network, TransportError};
use crate::utils::{Address, Dec};
use serde::Deserialize;
use serde_json::Value;
use super::*;
use crate::earn::staking::reads::{
Delegation, DelegatorSummary, MAINNET_DELEGATION_LOCK_MS, MAINNET_UNSTAKING_MS, Reward, StakeDelta, StakeEvent,
TESTNET_DELEGATION_LOCK_MS, TESTNET_UNSTAKING_MS, ValidatorSummary, delegation_lock_ms, locked_until, unstaking_ms,
};
const MAINNET: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/fixtures/stake/mainnet.json"));
const TESTNET: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/fixtures/stake/testnet.json"));
const DAY: u64 = 86_400_000;
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct Recorded {
#[serde(default)]
validator_summaries: Vec<ValidatorSummary>,
delegator_summary: DelegatorSummary,
delegations: Vec<Delegation>,
delegator_rewards: Vec<Reward>,
delegator_history: Vec<StakeEvent>,
}
fn recorded(network: Network) -> Recorded {
serde_json::from_str(match network {
Network::Mainnet => MAINNET,
Network::Testnet => TESTNET,
})
.unwrap()
}
fn dec(text: &str) -> Dec {
Dec::parse(text).unwrap()
}
fn address(text: &str) -> Address {
text.parse().unwrap()
}
fn queued(history: &[StakeEvent]) -> Vec<(u64, u64)> {
history
.iter()
.enumerate()
.filter(|(_, event)| matches!(&event.delta, StakeDelta::Withdrawal { phase, .. } if phase == "initiated"))
.filter_map(|(at, event)| {
let finalized = history[..at]
.iter()
.rev()
.find(|newer| matches!(&newer.delta, StakeDelta::Withdrawal { phase, .. } if phase == "finalized"))?;
Some((event.time, finalized.time))
})
.collect()
}
#[test]
fn each_network_s_unstaking_queue_is_what_its_history_shows() {
assert_eq!((MAINNET_UNSTAKING_MS, TESTNET_UNSTAKING_MS), (7 * DAY, 300_000));
for network in Network::ALL {
let history = recorded(network).delegator_history;
let spans = queued(&history);
assert!(!spans.is_empty(), "{network}");
for (initiated, finalized) in spans {
let ready = initiated + unstaking_ms(network);
assert!(ready <= finalized && finalized - ready < 1_000, "{network}: {initiated} to {finalized}");
}
}
}
#[test]
fn each_network_s_delegation_lock_is_what_its_delegations_show() {
assert_eq!((MAINNET_DELEGATION_LOCK_MS, TESTNET_DELEGATION_LOCK_MS), (DAY, 120_000));
for network in Network::ALL {
let recorded = recorded(network);
let mut checked = 0;
for delegation in &recorded.delegations {
let last = recorded.delegator_history.iter().find(|event| {
matches!(&event.delta, StakeDelta::Delegate { validator, is_undelegate: false, .. }
if *validator == delegation.validator)
});
let Some(last) = last else { continue };
assert_eq!(delegation.locked_until_timestamp, last.time + delegation_lock_ms(network), "{network}");
assert_eq!(locked_until(network, last.time), delegation.locked_until_timestamp, "{network}");
checked += 1;
}
assert!(checked > 0, "{network}: a delegation and its row are on file");
}
}
#[test]
fn a_withdrawal_is_pending_from_its_start_until_its_queue_ends() {
let mainnet = recorded(Network::Mainnet).delegator_history;
let started = 1_787_996_509_903;
let pending = pending_withdrawals(Network::Mainnet, &mainnet, started + 3 * DAY);
assert_eq!(
pending,
[PendingWithdrawal { amount: dec("500000"), initiated: started, ready_at: started + 7 * DAY }],
"four of seven days left"
);
assert_eq!(pending_withdrawals(Network::Mainnet, &mainnet, started - 1), [], "not yet asked for");
assert_eq!(pending_withdrawals(Network::Mainnet, &mainnet, 1_788_601_310_048), [], "finalized");
let testnet = recorded(Network::Testnet).delegator_history;
let started = 1_788_524_534_578;
let pending = pending_withdrawals(Network::Testnet, &testnet, started + 60_000);
assert_eq!(pending.len(), 1);
assert_eq!((pending[0].amount, pending[0].ready_at), (dec("10000"), started + 300_000));
assert_eq!(pending_withdrawals(Network::Testnet, &testnet, 1_788_524_835_009), [], "finalized in five minutes");
assert_eq!(
pending_withdrawals(Network::Mainnet, &testnet, 1_788_524_835_009).len(),
1,
"mainnet's queue would still hold it"
);
}
#[test]
fn rewards_are_summed_exactly_over_a_window() {
let mainnet = recorded(Network::Mainnet).delegator_rewards;
let now = 1_791_450_000_000;
assert_eq!(rewards_since(&mainnet, now - REWARDS_WINDOW_MS), Some(dec("909.24093562")));
let testnet = recorded(Network::Testnet).delegator_rewards;
assert_eq!(rewards_since(&testnet, 1_788_404_400_015), Some(dec("885.76419239")), "a row at the start counts");
assert_eq!(rewards_since(&testnet, u64::MAX), Some(Dec::ZERO));
}
#[test]
fn a_whole_staking_reads_from_four_answers() {
let recorded = recorded(Network::Mainnet);
let started = 1_787_996_509_903;
let mut summary = recorded.delegator_summary.clone();
summary.n_pending_withdrawals = 1;
let staking = Staking::new(
Network::Mainnet,
summary,
recorded.delegations.clone(),
&recorded.delegator_rewards,
&recorded.delegator_history,
started + DAY,
);
assert_eq!(staking.pending.len(), 1);
assert_eq!(staking.delegations.len(), 2);
assert_eq!(staking.summary.delegated, dec("501216.46877001"));
assert_eq!(staking.total(), Some(dec("501216.46877001")));
let none = Staking::new(
Network::Mainnet,
recorded.delegator_summary.clone(),
recorded.delegations.clone(),
&recorded.delegator_rewards,
&recorded.delegator_history,
started + DAY,
);
assert!(none.pending.is_empty(), "the summary counts none, so none is pending");
let delegated = recorded.delegations.iter().try_fold(Dec::ZERO, |sum, one| sum.checked_add(one.amount));
assert_eq!(delegated, Some(recorded.delegator_summary.delegated), "the delegations add up to the summary");
}
#[test]
fn testnet_validators_read() {
let validators = recorded(Network::Testnet).validator_summaries;
assert_eq!(validators.len(), 2);
let hypurr = validators.iter().find(|one| one.name == "Hypurr2").unwrap();
assert_eq!(hypurr.validator, address("0x172054cfc01b32effe0bf6af7a15b36e1ad730b3"));
assert_eq!(hypurr.stake_hype(), Some(dec("3210070.54148517")));
assert_eq!(hypurr.stats("week").map(|week| week.predicted_apr), Some(dec("0.0329726982")));
assert!(hypurr.is_active && !hypurr.is_jailed);
let jailed = validators.iter().find(|one| one.name == "somebody").unwrap();
assert!(jailed.is_jailed && !jailed.is_active && jailed.commission == Dec::ONE);
}
struct Recording(Value);
impl Transport for Recording {
fn post(&self, _: &str, body: String) -> impl Future<Output = Result<String, TransportError>> {
let request: Value = serde_json::from_str(&body).unwrap();
let kind = request["type"].as_str().unwrap();
assert_eq!(request["user"], "0x0594f60525fc6a5d05bb711fb6450f84fef307bd");
ready(Ok(self.0[kind].to_string()))
}
}
fn block_on<F: Future>(future: F) -> F::Output {
let mut future = pin!(future);
let mut context = Context::from_waker(Waker::noop());
loop {
if let Poll::Ready(value) = future.as_mut().poll(&mut context) {
return value;
}
}
}
#[test]
fn the_four_reads_make_one_staking() {
let transport = Recording(serde_json::from_str(TESTNET).unwrap());
let user = address("0x0594f60525fc6a5d05bb711fb6450f84fef307bd");
let staking = block_on(staking(&transport, Network::Testnet, user, 1_788_411_600_015)).unwrap();
assert_eq!(staking.summary.delegated, dec("968879.07903711"));
assert_eq!(staking.delegations[0].amount, dec("968879.07903711"));
assert_eq!(staking.rewards, dec("2151.13385194"), "all six rows fall in the month");
assert!(staking.pending.is_empty());
}