use chrono::{DateTime, Utc};
use rust_decimal::Decimal;
use rust_decimal_macros::dec;
use serde::{Deserialize, Serialize};
use sqlx::FromRow;
use std::fmt;
use uuid::Uuid;
use super::user::ValidationError;
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
pub struct VestingSchedule {
pub schedule_id: Uuid,
pub token_id: Uuid,
pub beneficiary_user_id: Uuid,
pub total_tokens: Decimal,
pub released_tokens: Decimal,
pub start_time: DateTime<Utc>,
pub cliff_duration_seconds: i64,
pub vesting_duration_seconds: i64,
pub revocable: bool,
pub revoked: bool,
pub vesting_type: VestingType,
pub milestone_conditions: Option<String>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, sqlx::Type, PartialEq, Eq)]
#[sqlx(type_name = "varchar", rename_all = "snake_case")]
#[derive(Default)]
pub enum VestingType {
#[default]
Linear,
Graded,
Milestone,
CliffOnly,
}
impl fmt::Display for VestingType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
VestingType::Linear => write!(f, "linear"),
VestingType::Graded => write!(f, "graded"),
VestingType::Milestone => write!(f, "milestone"),
VestingType::CliffOnly => write!(f, "cliff_only"),
}
}
}
impl VestingSchedule {
pub fn vested_amount(&self, at_time: DateTime<Utc>) -> Decimal {
if self.revoked {
return self.released_tokens; }
let elapsed = (at_time - self.start_time).num_seconds();
if elapsed < self.cliff_duration_seconds {
return dec!(0);
}
match self.vesting_type {
VestingType::Linear => self.linear_vested_amount(elapsed),
VestingType::Graded => self.graded_vested_amount(elapsed),
VestingType::Milestone => self.released_tokens, VestingType::CliffOnly => {
if elapsed >= self.cliff_duration_seconds {
self.total_tokens
} else {
dec!(0)
}
}
}
}
fn linear_vested_amount(&self, elapsed_seconds: i64) -> Decimal {
if elapsed_seconds >= self.vesting_duration_seconds {
return self.total_tokens;
}
let elapsed = Decimal::from(elapsed_seconds);
let total_duration = Decimal::from(self.vesting_duration_seconds);
(self.total_tokens * elapsed) / total_duration
}
fn graded_vested_amount(&self, elapsed_seconds: i64) -> Decimal {
if elapsed_seconds >= self.vesting_duration_seconds {
return self.total_tokens;
}
let num_grades = 4;
let grade_duration = self.vesting_duration_seconds / num_grades;
let completed_grades = elapsed_seconds / grade_duration;
let grade_amount = self.total_tokens / Decimal::from(num_grades);
grade_amount * Decimal::from(completed_grades)
}
pub fn releasable_amount(&self, at_time: DateTime<Utc>) -> Decimal {
let vested = self.vested_amount(at_time);
(vested - self.released_tokens).max(dec!(0))
}
pub fn is_vesting_complete(&self, at_time: DateTime<Utc>) -> bool {
self.vested_amount(at_time) >= self.total_tokens
}
pub fn vesting_progress(&self, at_time: DateTime<Utc>) -> Decimal {
if self.total_tokens == dec!(0) {
return dec!(1);
}
(self.vested_amount(at_time) / self.total_tokens).min(dec!(1))
}
pub fn time_remaining(&self, at_time: DateTime<Utc>) -> Option<i64> {
let end_time = self.start_time + chrono::Duration::seconds(self.vesting_duration_seconds);
if at_time >= end_time {
return None;
}
Some((end_time - at_time).num_seconds())
}
pub fn validate(&self) -> Result<(), ValidationError> {
if self.total_tokens <= dec!(0) {
return Err(ValidationError("Total tokens must be positive".to_string()));
}
if self.cliff_duration_seconds < 0 {
return Err(ValidationError(
"Cliff duration cannot be negative".to_string(),
));
}
if self.vesting_duration_seconds <= 0 {
return Err(ValidationError(
"Vesting duration must be positive".to_string(),
));
}
if self.cliff_duration_seconds > self.vesting_duration_seconds {
return Err(ValidationError(
"Cliff duration cannot exceed vesting duration".to_string(),
));
}
if self.released_tokens < dec!(0) {
return Err(ValidationError(
"Released tokens cannot be negative".to_string(),
));
}
if self.released_tokens > self.total_tokens {
return Err(ValidationError(
"Released tokens cannot exceed total tokens".to_string(),
));
}
Ok(())
}
}
impl fmt::Display for VestingSchedule {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"VestingSchedule({}, {}/{} tokens, type={})",
self.schedule_id, self.released_tokens, self.total_tokens, self.vesting_type
)
}
}
#[derive(Debug, Deserialize)]
pub struct CreateVestingScheduleRequest {
pub token_id: Uuid,
pub beneficiary_user_id: Uuid,
pub total_tokens: Decimal,
pub cliff_duration_seconds: i64,
pub vesting_duration_seconds: i64,
pub revocable: bool,
pub vesting_type: VestingType,
pub milestone_conditions: Option<String>,
}
impl CreateVestingScheduleRequest {
pub fn validate(&self) -> Result<(), ValidationError> {
if self.total_tokens <= dec!(0) {
return Err(ValidationError("Total tokens must be positive".to_string()));
}
if self.cliff_duration_seconds < 0 {
return Err(ValidationError(
"Cliff duration cannot be negative".to_string(),
));
}
if self.vesting_duration_seconds <= 0 {
return Err(ValidationError(
"Vesting duration must be positive".to_string(),
));
}
if self.cliff_duration_seconds > self.vesting_duration_seconds {
return Err(ValidationError(
"Cliff duration cannot exceed vesting duration".to_string(),
));
}
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
pub struct VestingRelease {
pub release_id: Uuid,
pub schedule_id: Uuid,
pub user_id: Uuid,
pub amount: Decimal,
pub released_at: DateTime<Utc>,
}
impl fmt::Display for VestingRelease {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"VestingRelease({}, amount={}, at={})",
self.release_id, self.amount, self.released_at
)
}
}
#[derive(Debug, Serialize)]
pub struct VestingSummary {
pub user_id: Uuid,
pub total_schedules: i32,
pub total_vesting_tokens: Decimal,
pub total_vested_tokens: Decimal,
pub total_released_tokens: Decimal,
pub total_releasable_tokens: Decimal,
pub schedules: Vec<VestingSchedule>,
}
pub struct VestingTemplates;
impl VestingTemplates {
pub fn standard_four_year() -> (i64, i64, VestingType) {
const ONE_YEAR: i64 = 365 * 24 * 60 * 60;
const FOUR_YEARS: i64 = 4 * ONE_YEAR;
(ONE_YEAR, FOUR_YEARS, VestingType::Linear)
}
pub fn quarterly_one_year() -> (i64, i64, VestingType) {
const THREE_MONTHS: i64 = 90 * 24 * 60 * 60;
const ONE_YEAR: i64 = 365 * 24 * 60 * 60;
(THREE_MONTHS, ONE_YEAR, VestingType::Graded)
}
pub fn monthly_two_years() -> (i64, i64, VestingType) {
const TWO_YEARS: i64 = 2 * 365 * 24 * 60 * 60;
(0, TWO_YEARS, VestingType::Linear)
}
pub fn immediate() -> (i64, i64, VestingType) {
(0, 1, VestingType::CliffOnly)
}
pub fn six_month_cliff() -> (i64, i64, VestingType) {
const SIX_MONTHS: i64 = 180 * 24 * 60 * 60;
(SIX_MONTHS, SIX_MONTHS, VestingType::CliffOnly)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_linear_vesting() {
let now = Utc::now();
let schedule = VestingSchedule {
schedule_id: Uuid::new_v4(),
token_id: Uuid::new_v4(),
beneficiary_user_id: Uuid::new_v4(),
total_tokens: dec!(1000),
released_tokens: dec!(0),
start_time: now,
cliff_duration_seconds: 0,
vesting_duration_seconds: 100,
revocable: false,
revoked: false,
vesting_type: VestingType::Linear,
milestone_conditions: None,
created_at: now,
updated_at: now,
};
assert_eq!(schedule.vested_amount(now), dec!(0));
let halfway = now + chrono::Duration::seconds(50);
assert_eq!(schedule.vested_amount(halfway), dec!(500));
let end = now + chrono::Duration::seconds(100);
assert_eq!(schedule.vested_amount(end), dec!(1000));
let after = now + chrono::Duration::seconds(200);
assert_eq!(schedule.vested_amount(after), dec!(1000));
}
#[test]
fn test_cliff_vesting() {
let now = Utc::now();
let schedule = VestingSchedule {
schedule_id: Uuid::new_v4(),
token_id: Uuid::new_v4(),
beneficiary_user_id: Uuid::new_v4(),
total_tokens: dec!(1000),
released_tokens: dec!(0),
start_time: now,
cliff_duration_seconds: 50,
vesting_duration_seconds: 100,
revocable: false,
revoked: false,
vesting_type: VestingType::Linear,
milestone_conditions: None,
created_at: now,
updated_at: now,
};
let before_cliff = now + chrono::Duration::seconds(25);
assert_eq!(schedule.vested_amount(before_cliff), dec!(0));
let after_cliff = now + chrono::Duration::seconds(75);
assert_eq!(schedule.vested_amount(after_cliff), dec!(750));
}
#[test]
fn test_graded_vesting() {
let now = Utc::now();
let schedule = VestingSchedule {
schedule_id: Uuid::new_v4(),
token_id: Uuid::new_v4(),
beneficiary_user_id: Uuid::new_v4(),
total_tokens: dec!(1000),
released_tokens: dec!(0),
start_time: now,
cliff_duration_seconds: 0,
vesting_duration_seconds: 100,
revocable: false,
revoked: false,
vesting_type: VestingType::Graded,
milestone_conditions: None,
created_at: now,
updated_at: now,
};
assert_eq!(schedule.vested_amount(now), dec!(0));
let quarter = now + chrono::Duration::seconds(25);
assert_eq!(schedule.vested_amount(quarter), dec!(250));
let half = now + chrono::Duration::seconds(50);
assert_eq!(schedule.vested_amount(half), dec!(500));
}
#[test]
fn test_releasable_amount() {
let now = Utc::now();
let mut schedule = VestingSchedule {
schedule_id: Uuid::new_v4(),
token_id: Uuid::new_v4(),
beneficiary_user_id: Uuid::new_v4(),
total_tokens: dec!(1000),
released_tokens: dec!(300),
start_time: now,
cliff_duration_seconds: 0,
vesting_duration_seconds: 100,
revocable: false,
revoked: false,
vesting_type: VestingType::Linear,
milestone_conditions: None,
created_at: now,
updated_at: now,
};
let halfway = now + chrono::Duration::seconds(50);
assert_eq!(schedule.releasable_amount(halfway), dec!(200));
schedule.released_tokens = dec!(500);
assert_eq!(schedule.releasable_amount(halfway), dec!(0));
}
#[test]
fn test_vesting_templates() {
let (cliff, duration, vtype) = VestingTemplates::standard_four_year();
assert_eq!(cliff, 365 * 24 * 60 * 60);
assert_eq!(duration, 4 * 365 * 24 * 60 * 60);
assert_eq!(vtype, VestingType::Linear);
let (cliff, _duration, vtype) = VestingTemplates::immediate();
assert_eq!(cliff, 0);
assert_eq!(vtype, VestingType::CliffOnly);
}
}