cyb/src/pages/Mining/utils/emission.ts

/**
 * 1:1 TypeScript port of litium-mine/src/emission.rs
 * All pure functions, zero dependencies.
 *
 * Validated: totalRateAtomicPerSecond(0) === 4_080_207_780_836_639
 */

const LI_TOTAL_SUPPLY_TOKENS = 1_000_000_000_000_000;
const LI_DECIMALS = 1_000_000;
const LI_TOTAL_SUPPLY_ATOMIC = LI_TOTAL_SUPPLY_TOKENS * LI_DECIMALS;
const COMPONENT_ALLOC_ATOMIC = LI_TOTAL_SUPPLY_ATOMIC / 7;

const MAIN_PHASE_SHARE_NUM = 0.9;
const TAIL_MONTHLY_SHARE = 0.01;
const MONTH_DAYS = 30.0;
const INFINITE_COMPONENT_YEARS = 20.0;
const SECONDS_PER_DAY = 86_400.0;

const FINITE_PERIODS = [1, 7, 30, 90, 365, 1461] as const;

type FiniteProfile = {
  alloc: number;
  lambda: number;
  tailK: number;
  crossoverSeconds: number;
  emittedAtCrossover: number;
  remainingAtCrossover: number;
};

function infiniteComponentDurationSeconds(): number {
  return INFINITE_COMPONENT_YEARS * 365.0 * SECONDS_PER_DAY;
}

function finiteProfile(periodDays: number): FiniteProfile {
  const alloc = COMPONENT_ALLOC_ATOMIC;
  const periodSeconds = periodDays * SECONDS_PER_DAY;
  const lambda = Math.log(10) / periodSeconds;
  const tailK = TAIL_MONTHLY_SHARE / (MONTH_DAYS * SECONDS_PER_DAY);
  const x = Math.max(0, Math.min(1, tailK / (9.0 * (lambda - tailK))));
  const crossoverSeconds = x > 0 ? -Math.log(x) / lambda : 0;
  const emittedAtCrossover =
    MAIN_PHASE_SHARE_NUM * alloc * (1 - Math.exp(-lambda * crossoverSeconds));
  const remainingAtCrossover = Math.max(0, alloc - emittedAtCrossover);

  return {
    alloc,
    lambda,
    tailK,
    crossoverSeconds,
    emittedAtCrossover,
    remainingAtCrossover,
  };
}

export function finiteComponentRate(
  elapsedSeconds: number,
  periodDays: number
): number {
  const p = finiteProfile(periodDays);
  if (elapsedSeconds <= p.crossoverSeconds) {
    return (
      MAIN_PHASE_SHARE_NUM *
      p.alloc *
      p.lambda *
      Math.exp(-p.lambda * elapsedSeconds)
    );
  }
  return (
    p.remainingAtCrossover *
    p.tailK *
    Math.exp(-p.tailK * (elapsedSeconds - p.crossoverSeconds))
  );
}

function finiteComponentEmitted(
  elapsedSeconds: number,
  periodDays: number
): number {
  const t = Math.max(0, elapsedSeconds);
  const p = finiteProfile(periodDays);
  if (t <= p.crossoverSeconds) {
    return MAIN_PHASE_SHARE_NUM * p.alloc * (1 - Math.exp(-p.lambda * t));
  }
  return (
    p.emittedAtCrossover +
    p.remainingAtCrossover *
      (1 - Math.exp(-p.tailK * (t - p.crossoverSeconds)))
  );
}

export function infiniteComponentRate(elapsedSeconds: number): number {
  if (elapsedSeconds >= infiniteComponentDurationSeconds()) {
    return 0;
  }
  return COMPONENT_ALLOC_ATOMIC / infiniteComponentDurationSeconds();
}

function infiniteComponentEmitted(elapsedSeconds: number): number {
  const duration = infiniteComponentDurationSeconds();
  const t = Math.max(0, Math.min(duration, elapsedSeconds));
  return COMPONENT_ALLOC_ATOMIC * (t / duration);
}

/** Total emission rate in atomic LI per second at the given elapsed time. */
export function totalRateAtomicPerSecond(elapsedSeconds: number): number {
  let sum = 0;
  for (const pd of FINITE_PERIODS) {
    sum += finiteComponentRate(elapsedSeconds, pd);
  }
  sum += infiniteComponentRate(elapsedSeconds);
  return sum;
}

/** Total cumulative emitted atomic LI at the given elapsed time. */
export function totalEmittedAtomic(elapsedSeconds: number): number {
  let sum = 0;
  for (const pd of FINITE_PERIODS) {
    sum += finiteComponentEmitted(elapsedSeconds, pd);
  }
  sum += infiniteComponentEmitted(elapsedSeconds);
  return sum;
}

export type ComponentRate = {
  name: string;
  periodDays: number;
  rate: number; // atomic LI/s
  color: string;
};

const COMPONENT_COLORS: Record<number, string> = {
  1: '#ef4444',
  7: '#f97316',
  30: '#eab308',
  90: '#22c55e',
  365: '#3b82f6',
  1461: '#a855f7',
  0: '#ec4899', // infinite
};

const COMPONENT_NAMES: Record<number, string> = {
  1: 'Li\u2081',
  7: 'Li\u2087',
  30: 'Li\u2083\u2080',
  90: 'Li\u2089\u2080',
  365: 'Li\u2083\u2086\u2085',
  1461: 'Li\u2081\u2084\u2086\u2081',
  0: 'Li\u221E',
};

/** Per-component rate breakdown at given elapsed time (7 components). */
export function componentRates(elapsedSeconds: number): ComponentRate[] {
  const rates: ComponentRate[] = FINITE_PERIODS.map((pd) => ({
    name: COMPONENT_NAMES[pd],
    periodDays: pd,
    rate: finiteComponentRate(elapsedSeconds, pd),
    color: COMPONENT_COLORS[pd],
  }));
  rates.push({
    name: COMPONENT_NAMES[0],
    periodDays: 0,
    rate: infiniteComponentRate(elapsedSeconds),
    color: COMPONENT_COLORS[0],
  });
  return rates;
}

/** Convert atomic LI to human LI. */
export function atomicToHuman(atomic: number): number {
  return atomic / LI_DECIMALS;
}

export { LI_TOTAL_SUPPLY_ATOMIC, LI_DECIMALS, FINITE_PERIODS, SECONDS_PER_DAY };

Neighbours