@ickb/utils
Version:
General utilities built on top of CCC
245 lines (218 loc) • 7.48 kB
text/typescript
import { ccc } from "@ckb-ccc/core";
import { gcd } from "./utils.js";
/**
* Represents an Epoch in two possible forms:
* - An object with { number, index, length } values.
* - A native ccc.Epoch.
*/
export type EpochLike =
| {
number: ccc.Num;
index: ccc.Num;
length: ccc.Num;
}
| ccc.Epoch;
/**
* Class representing an Epoch that tracks a value composed of a whole
* number and a normalized fractional part.
*
* The Epoch is stored as three components:
* - number: the whole number part,
* - index: the numerator of the fractional part, and
* - length: the denominator of the fractional part.
*
* The class provides static factory methods to construct an Epoch from
* different representations (including a hexadecimal representation) and
* implements methods to add, subtract, normalize, compare, and convert to a
* hexadecimal representation, in addition to converting the epoch to a timestamp.
*/
export class Epoch {
/**
* Create an Epoch instance.
* @param number - The whole number part.
* @param index - The fractional numerator.
* @param length - The fractional denominator.
*/
private constructor(
public readonly number: ccc.Num,
public readonly index: ccc.Num,
public readonly length: ccc.Num,
) {}
/**
* Create an Epoch instance from an EpochLike representation.
*
* The method first de-structures the passed value into the standard tuple,
* then performs normalization:
* - Ensures the epoch length is positive.
* - Corrects negative index by borrowing from the whole number.
* - Reduces the fractional part using the greatest common divisor.
* - Carries over any overflow from the fraction.
*
* @param epochLike - The EpochLike value to convert.
* @returns A normalized Epoch instance.
* @throws Error if the epoch length is non-positive.
*/
static from(epochLike: EpochLike): Epoch {
if (epochLike instanceof Epoch) {
return epochLike;
}
let { number, index, length } = deStruct(epochLike);
// Ensure the epoch has a positive denominator.
if (length <= 0n) {
throw new Error("Non positive Epoch length");
}
// Normalize negative index values by borrowing from the whole number.
if (index < 0n) {
// Calculate how many whole units to borrow.
const n = (-index + length - 1n) / length;
number -= n;
index += length * n;
}
// Reduce the fraction (index / length) to its simplest form using the greatest common divisor.
const g = gcd(index, length);
index /= g;
length /= g;
// Add any whole number overflow from the fraction.
number += index / length;
// Calculate the leftover index after accounting for the whole number part from the fraction.
index %= length;
return new Epoch(number, index, length);
}
/**
* Create an Epoch from a hexadecimal string representation.
*
* @param hex - The hexadecimal representation of the epoch.
* @returns A normalized Epoch instance.
*/
static fromHex(hex: ccc.Hex): Epoch {
return Epoch.from(ccc.epochFromHex(hex));
}
/**
* Convert this Epoch instance to its hexadecimal string representation.
*
* @returns The hexadecimal representation of this epoch.
*/
toHex(): ccc.Hex {
const { number, index, length } = this;
return ccc.epochToHex([number, index, length]);
}
/**
* Compare this epoch with another Epoch (or EpochLike).
*
* The comparison first checks the whole number parts. If they are equal,
* it compares the fractions by cross-multiplying the indices with the denominators.
*
* @param other - The epoch or epoch-like to compare with.
* @returns 1 if this epoch is greater, -1 if less, and 0 if equal.
*/
compare(other: EpochLike): 1 | 0 | -1 {
other = Epoch.from(other);
if (this.number < other.number) {
return -1;
}
if (this.number > other.number) {
return 1;
}
// Compare fractions by cross-multiplying indices with denominators.
const v0 = this.index * other.length;
const v1 = other.index * this.length;
if (v0 < v1) {
return -1;
}
if (v0 > v1) {
return 1;
}
return 0;
}
/**
* Add another Epoch (or EpochLike) to this epoch.
*
* When adding, the whole number parts are directly summed. If the epochs have different
* denominators (lengths), the fractions are first aligned to a common denominator, then
* normalized.
*
* @param other - The epoch or epoch-like value to add.
* @returns A new normalized Epoch instance representing the sum.
*/
add(other: EpochLike): Epoch {
other = Epoch.from(other);
// Sum the whole number parts.
const number = this.number + other.number;
let index: ccc.Num;
let length: ccc.Num;
// If the epochs have different denominators (lengths), align them to a common denominator.
if (this.length !== other.length) {
index = other.index * this.length + this.index * other.length;
length = this.length * other.length;
} else {
// If denominators are equal, simply add the indices.
index = this.index + other.index;
length = this.length;
}
// Normalize the resulting epoch tuple.
return Epoch.from([number, index, length]);
}
/**
* Subtract an Epoch (or EpochLike) from this epoch.
*
* This method destructures the provided epoch-like value and then negates the respective
* components before adding them to this epoch.
*
* @param other - The epoch or epoch-like value to subtract.
* @returns A new normalized Epoch instance representing the difference.
*/
sub(other: EpochLike): Epoch {
// Destructure delta into its constituents.
const { number, index, length } = deStruct(other);
return this.add([-number, -index, length]);
}
/**
* Convert this epoch to an absolute Unix timestamp.
*
* For a given reference block header, the conversion computes the difference between
* this epoch and the reference epoch, then applies a per-epoch millisecond duration to
* calculate the absolute Unix timestamp.
*
* @param reference - The reference client block header providing an epoch and timestamp.
* @returns The calculated Unix timestamp as a bigint.
*/
toUnix(reference: ccc.ClientBlockHeader): bigint {
// Calculate the difference between the provided epoch and the reference epoch.
const { number, index, length } = this.sub(reference.epoch);
return (
reference.timestamp +
epochInMilliseconds * number +
(epochInMilliseconds * index) / length
);
}
}
/**
* Deconstruct an EpochLike value into its constitutive parts.
*
* The function handles both array representations and object representations of an epoch.
*
* @param epochLike - The epoch-like structure to deconstruct.
* @returns An object containing { number, index, length }.
*/
function deStruct(epochLike: EpochLike): {
number: ccc.Num;
index: ccc.Num;
length: ccc.Num;
} {
if (epochLike instanceof Array) {
const [number, index, length] = epochLike;
return {
number,
index,
length,
};
}
return epochLike;
}
/**
* A constant representing the epoch duration in milliseconds.
*
* Calculated as 4 hours in milliseconds:
* 4 hours * 60 minutes per hour * 60 seconds per minute * 1000 milliseconds per second.
*/
const epochInMilliseconds = 14400000n;