use cosmwasm_std::Uint128;

/// Lithium total supply in LI tokens (human units, not atomic).
const LI_TOTAL_SUPPLY_TOKENS: u128 = 1_000_000_000_000_000;
/// Contract token decimals.
const LI_DECIMALS: u128 = 1_000_000;
/// Atomic total supply.
pub const LI_TOTAL_SUPPLY_ATOMIC: u128 = LI_TOTAL_SUPPLY_TOKENS * LI_DECIMALS;
/// Equal allocation across 7 emission components.
pub const COMPONENT_ALLOC_ATOMIC: u128 = LI_TOTAL_SUPPLY_ATOMIC / 7;

/// 90% of each finite component uses exponential main phase.
const MAIN_PHASE_SHARE_NUM: f64 = 0.9;
/// Tail emission speed: 1% / month of remaining supply.
const TAIL_MONTHLY_SHARE: f64 = 0.01;
/// Average month length in days used in spec.
const MONTH_DAYS: f64 = 30.0;
/// Li∞ spreads allocation over ~20 years linearly.
const INFINITE_COMPONENT_YEARS: f64 = 20.0;
/// Seconds per day.
pub const SECONDS_PER_DAY: f64 = 86400.0;

#[derive(Debug, Clone, Copy)]
struct FiniteProfile {
    alloc: f64,
    lambda: f64,
    tail_k: f64,
    crossover_t: f64,
    emitted_at_crossover: f64,
    remaining_at_crossover: f64,
}

/// Returns emission rate in atomic LI per second at a given point in time.
/// `genesis_time` and `now` are both unix timestamps in seconds.
pub fn emission_rate_per_second(genesis_time: u64, now: u64) -> Uint128 {
    let elapsed_seconds = now.saturating_sub(genesis_time) as f64;
    let rate = total_rate_at_time(elapsed_seconds);
    let atomic = f64_to_u128_saturating(rate);
    Uint128::from(atomic)
}

/// Returns total emitted atomic LI from genesis to `now`.
pub fn total_emitted_at_time(genesis_time: u64, now: u64) -> Uint128 {
    let elapsed_seconds = now.saturating_sub(genesis_time) as f64;
    let emitted = total_emitted_atomic(elapsed_seconds);
    Uint128::from(f64_to_u128_saturating(emitted))
}

/// Instantaneous total emission rate (atomic LI per second) at elapsed_seconds from genesis.
pub fn total_rate_at_time(elapsed_seconds: f64) -> f64 {
    finite_component_rate(elapsed_seconds, 1.0)
        + finite_component_rate(elapsed_seconds, 7.0)
        + finite_component_rate(elapsed_seconds, 30.0)
        + finite_component_rate(elapsed_seconds, 90.0)
        + finite_component_rate(elapsed_seconds, 365.0)
        + finite_component_rate(elapsed_seconds, 1461.0)
        + infinite_component_rate(elapsed_seconds)
}

/// Cumulative total emitted (atomic LI) at elapsed_seconds from genesis.
fn total_emitted_atomic(elapsed_seconds: f64) -> f64 {
    finite_component_emitted(elapsed_seconds, 1.0)
        + finite_component_emitted(elapsed_seconds, 7.0)
        + finite_component_emitted(elapsed_seconds, 30.0)
        + finite_component_emitted(elapsed_seconds, 90.0)
        + finite_component_emitted(elapsed_seconds, 365.0)
        + finite_component_emitted(elapsed_seconds, 1461.0)
        + infinite_component_emitted(elapsed_seconds)
}

fn finite_component_emitted(elapsed_seconds: f64, period_days: f64) -> f64 {
    let t = elapsed_seconds.max(0.0);
    let p = finite_profile(period_days);
    if t <= p.crossover_t {
        MAIN_PHASE_SHARE_NUM * p.alloc * (1.0 - f64::exp(-p.lambda * t))
    } else {
        p.emitted_at_crossover
            + p.remaining_at_crossover * (1.0 - f64::exp(-p.tail_k * (t - p.crossover_t)))
    }
}

pub fn finite_component_rate(elapsed_seconds: f64, period_days: f64) -> f64 {
    let t = elapsed_seconds.max(0.0);
    let p = finite_profile(period_days);
    if t <= p.crossover_t {
        MAIN_PHASE_SHARE_NUM * p.alloc * p.lambda * f64::exp(-p.lambda * t)
    } else {
        p.remaining_at_crossover * p.tail_k * f64::exp(-p.tail_k * (t - p.crossover_t))
    }
}

fn infinite_component_emitted(elapsed_seconds: f64) -> f64 {
    let alloc = COMPONENT_ALLOC_ATOMIC as f64;
    let duration = infinite_component_duration_seconds();
    let t = elapsed_seconds.max(0.0).min(duration);
    alloc * (t / duration)
}

pub fn infinite_component_rate(elapsed_seconds: f64) -> f64 {
    if elapsed_seconds >= infinite_component_duration_seconds() {
        return 0.0;
    }
    let alloc = COMPONENT_ALLOC_ATOMIC as f64;
    alloc / infinite_component_duration_seconds()
}

fn finite_profile(period_days: f64) -> FiniteProfile {
    let alloc = COMPONENT_ALLOC_ATOMIC as f64;
    let period_seconds = period_days * SECONDS_PER_DAY;
    let lambda = f64::ln(10.0) / period_seconds;
    let tail_k = TAIL_MONTHLY_SHARE / (MONTH_DAYS * SECONDS_PER_DAY);
    let x = (tail_k / (9.0 * (lambda - tail_k))).clamp(0.0, 1.0);
    let crossover_t = if x > 0.0 { -x.ln() / lambda } else { 0.0 };
    let emitted_at_crossover =
        MAIN_PHASE_SHARE_NUM * alloc * (1.0 - f64::exp(-lambda * crossover_t));
    let remaining_at_crossover = (alloc - emitted_at_crossover).max(0.0);

    FiniteProfile {
        alloc,
        lambda,
        tail_k,
        crossover_t,
        emitted_at_crossover,
        remaining_at_crossover,
    }
}

fn infinite_component_duration_seconds() -> f64 {
    INFINITE_COMPONENT_YEARS * 365.0 * SECONDS_PER_DAY
}

fn f64_to_u128_saturating(v: f64) -> u128 {
    if !v.is_finite() || v <= 0.0 {
        return 0;
    }
    if v >= u128::MAX as f64 {
        return u128::MAX;
    }
    v.floor() as u128
}

Local Graph