|
| 1 | +import type { ClientBlockHeader } from "../client/clientTypes.js"; |
| 2 | +import { Zero } from "../fixedPoint/index.js"; |
| 3 | +import { type Hex, type HexLike } from "../hex/index.js"; |
| 4 | +import { Codec } from "../molecule/codec.js"; |
| 5 | +import { mol } from "../molecule/index.js"; |
| 6 | +import { numFrom, NumLike, type Num } from "../num/index.js"; |
| 7 | +import { gcd } from "../utils/index.js"; |
| 8 | + |
| 9 | +/** |
| 10 | + * @deprecated use `Epoch.from` instead |
| 11 | + * Convert an Epoch-like value into an Epoch instance. |
| 12 | + * |
| 13 | + * @param epochLike - An EpochLike value (object or tuple). |
| 14 | + * @returns An Epoch instance built from `epochLike`. |
| 15 | + */ |
| 16 | +export function epochFrom(epochLike: EpochLike): Epoch { |
| 17 | + return Epoch.from(epochLike); |
| 18 | +} |
| 19 | + |
| 20 | +/** |
| 21 | + * @deprecated use `Epoch.decode` instead |
| 22 | + * Decode an epoch from a hex-like representation. |
| 23 | + * |
| 24 | + * @param hex - A hex-like value representing an encoded epoch. |
| 25 | + * @returns An Epoch instance decoded from `hex`. |
| 26 | + */ |
| 27 | +export function epochFromHex(hex: HexLike): Epoch { |
| 28 | + return Epoch.decode(hex); |
| 29 | +} |
| 30 | + |
| 31 | +/** |
| 32 | + * @deprecated use `Epoch.from(epochLike).toHex` instead |
| 33 | + * Convert an Epoch-like value to its hex representation. |
| 34 | + * |
| 35 | + * @param epochLike - An EpochLike value (object, tuple, or Epoch). |
| 36 | + * @returns Hex string representing the epoch. |
| 37 | + */ |
| 38 | +export function epochToHex(epochLike: EpochLike): Hex { |
| 39 | + return Epoch.from(epochLike).toHex(); |
| 40 | +} |
| 41 | + |
| 42 | +export type EpochLike = |
| 43 | + | { |
| 44 | + integer: NumLike; |
| 45 | + numerator: NumLike; |
| 46 | + denominator: NumLike; |
| 47 | + } |
| 48 | + | [NumLike, NumLike, NumLike]; |
| 49 | + |
| 50 | +@mol.codec( |
| 51 | + mol |
| 52 | + .struct({ |
| 53 | + padding: Codec.from({ |
| 54 | + byteLength: 1, |
| 55 | + encode: (_) => new Uint8Array(1), |
| 56 | + decode: (_) => "0x00", |
| 57 | + }), |
| 58 | + value: mol.struct({ |
| 59 | + denominator: mol.Uint16LE, |
| 60 | + numerator: mol.Uint16LE, |
| 61 | + integer: mol.uint(3, true), |
| 62 | + }), |
| 63 | + }) |
| 64 | + .mapIn((encodable: EpochLike) => { |
| 65 | + const value = Epoch.from(encodable); |
| 66 | + return { |
| 67 | + padding: "0x00", |
| 68 | + value, |
| 69 | + }; |
| 70 | + }) |
| 71 | + .mapOut((v) => v.value as Epoch), |
| 72 | +) |
| 73 | +/** |
| 74 | + * Epoch |
| 75 | + * |
| 76 | + * Represents a timestamp-like epoch as a mixed whole integer and fractional part: |
| 77 | + * - integer: whole units |
| 78 | + * - numerator: numerator of the fractional part |
| 79 | + * - denominator: denominator of the fractional part (must be > 0) |
| 80 | + * |
| 81 | + * The fractional portion is numerator/denominator. Instances normalize fractions where |
| 82 | + * appropriate (e.g., reduce by GCD, carry whole units). |
| 83 | + */ |
| 84 | +export class Epoch extends mol.Entity.Base<EpochLike, Epoch>() { |
| 85 | + /** |
| 86 | + * Construct a new Epoch. |
| 87 | + * |
| 88 | + * The constructor enforces a positive `denominator`. If `denominator` |
| 89 | + * is non-positive an Error is thrown. |
| 90 | + * |
| 91 | + * @param integer - Whole number portion of the epoch. |
| 92 | + * @param numerator - Fractional numerator. |
| 93 | + * @param denominator - Fractional denominator (must be > 0). |
| 94 | + */ |
| 95 | + public constructor( |
| 96 | + public readonly integer: Num, |
| 97 | + public readonly numerator: Num, |
| 98 | + public readonly denominator: Num, |
| 99 | + ) { |
| 100 | + // Ensure the epoch has a positive denominator. |
| 101 | + if (denominator <= Zero) { |
| 102 | + throw new Error("Non positive Epoch denominator"); |
| 103 | + } |
| 104 | + super(); |
| 105 | + } |
| 106 | + |
| 107 | + /** |
| 108 | + * @deprecated use `integer` instead |
| 109 | + * Backwards-compatible array-style index 0 referencing the whole epoch integer. |
| 110 | + */ |
| 111 | + get 0(): Num { |
| 112 | + return this.integer; |
| 113 | + } |
| 114 | + |
| 115 | + /** |
| 116 | + * @deprecated use `numerator` instead |
| 117 | + * Backwards-compatible array-style index 1 referencing the epoch fractional numerator. |
| 118 | + */ |
| 119 | + get 1(): Num { |
| 120 | + return this.numerator; |
| 121 | + } |
| 122 | + |
| 123 | + /** |
| 124 | + * @deprecated use `denominator` instead |
| 125 | + * Backwards-compatible array-style index 2 referencing the epoch fractional denominator. |
| 126 | + */ |
| 127 | + get 2(): Num { |
| 128 | + return this.denominator; |
| 129 | + } |
| 130 | + |
| 131 | + /** |
| 132 | + * Create an Epoch from an EpochLike value. |
| 133 | + * |
| 134 | + * Accepts: |
| 135 | + * - an Epoch instance (returned as-is) |
| 136 | + * - an object { integer, numerator, denominator } where each field is NumLike |
| 137 | + * - a tuple [integer, numerator, denominator] where each element is NumLike |
| 138 | + * |
| 139 | + * All returned fields are converted to `Num` using `numFrom`. |
| 140 | + * |
| 141 | + * @param epochLike - Value to convert into an Epoch. |
| 142 | + * @returns A new or existing Epoch instance. |
| 143 | + */ |
| 144 | + static from(epochLike: EpochLike): Epoch { |
| 145 | + if (epochLike instanceof Epoch) { |
| 146 | + return epochLike; |
| 147 | + } |
| 148 | + |
| 149 | + let integer: NumLike, numerator: NumLike, denominator: NumLike; |
| 150 | + if (Array.isArray(epochLike)) { |
| 151 | + [integer, numerator, denominator] = epochLike; |
| 152 | + } else { |
| 153 | + ({ integer, numerator, denominator } = epochLike); |
| 154 | + } |
| 155 | + |
| 156 | + return new Epoch( |
| 157 | + numFrom(integer), |
| 158 | + numFrom(numerator), |
| 159 | + numFrom(denominator), |
| 160 | + ); |
| 161 | + } |
| 162 | + |
| 163 | + /** |
| 164 | + * Return an epoch representing zero (0 + 0/1). |
| 165 | + */ |
| 166 | + static zero(): Epoch { |
| 167 | + return new Epoch(0n, 0n, numFrom(1)); |
| 168 | + } |
| 169 | + |
| 170 | + /** |
| 171 | + * Return an epoch representing one cycle (180 + 0/1). |
| 172 | + * |
| 173 | + * This is a NervosDAO convenience constant. |
| 174 | + */ |
| 175 | + static oneNervosDaoCycle(): Epoch { |
| 176 | + return new Epoch(numFrom(180), 0n, numFrom(1)); |
| 177 | + } |
| 178 | + |
| 179 | + /** |
| 180 | + * Compare this epoch to another EpochLike. |
| 181 | + * |
| 182 | + * Comparison is performed by converting both epochs to a common integer |
| 183 | + * representation: (integer * denominator + numerator) scaled by the other's denominator. |
| 184 | + * |
| 185 | + * @param other - EpochLike value to compare against. |
| 186 | + * @returns 1 if this > other, 0 if equal, -1 if this < other. |
| 187 | + */ |
| 188 | + compare(other: EpochLike): 1 | 0 | -1 { |
| 189 | + if (this === other) { |
| 190 | + return 0; |
| 191 | + } |
| 192 | + |
| 193 | + const o = Epoch.from(other); |
| 194 | + const a = |
| 195 | + (this.integer * this.denominator + this.numerator) * o.denominator; |
| 196 | + const b = (o.integer * o.denominator + o.numerator) * this.denominator; |
| 197 | + |
| 198 | + return a > b ? 1 : a < b ? -1 : 0; |
| 199 | + } |
| 200 | + |
| 201 | + /** |
| 202 | + * Check whether this epoch is less than another EpochLike. |
| 203 | + * |
| 204 | + * @param other - EpochLike to compare against. |
| 205 | + * @returns true if this epoch is strictly less than the other. |
| 206 | + */ |
| 207 | + lt(other: EpochLike): boolean { |
| 208 | + return this.compare(other) < 0; |
| 209 | + } |
| 210 | + |
| 211 | + /** |
| 212 | + * Check whether this epoch is less than or equal to another EpochLike. |
| 213 | + * |
| 214 | + * @param other - EpochLike to compare against. |
| 215 | + * @returns true if this epoch is less than or equal to the other. |
| 216 | + */ |
| 217 | + le(other: EpochLike): boolean { |
| 218 | + return this.compare(other) <= 0; |
| 219 | + } |
| 220 | + |
| 221 | + /** |
| 222 | + * Check whether this epoch is equal to another EpochLike. |
| 223 | + * |
| 224 | + * @param other - EpochLike to compare against. |
| 225 | + * @returns true if both epochs represent the same value. |
| 226 | + */ |
| 227 | + eq(other: EpochLike): boolean { |
| 228 | + return this.compare(other) === 0; |
| 229 | + } |
| 230 | + |
| 231 | + /** |
| 232 | + * Check whether this epoch is greater than or equal to another EpochLike. |
| 233 | + * |
| 234 | + * @param other - EpochLike to compare against. |
| 235 | + * @returns true if this epoch is greater than or equal to the other. |
| 236 | + */ |
| 237 | + ge(other: EpochLike): boolean { |
| 238 | + return this.compare(other) >= 0; |
| 239 | + } |
| 240 | + |
| 241 | + /** |
| 242 | + * Check whether this epoch is greater than another EpochLike. |
| 243 | + * |
| 244 | + * @param other - EpochLike to compare against. |
| 245 | + * @returns true if this epoch is strictly greater than the other. |
| 246 | + */ |
| 247 | + gt(other: EpochLike): boolean { |
| 248 | + return this.compare(other) > 0; |
| 249 | + } |
| 250 | + |
| 251 | + /** |
| 252 | + * Return a normalized epoch: |
| 253 | + * - Ensures numerator is non-negative by borrowing from `integer` if needed. |
| 254 | + * - Reduces the fraction (numerator/denominator) by their GCD. |
| 255 | + * - Carries any whole units from the fraction into `integer`. |
| 256 | + * |
| 257 | + * @returns A new, normalized Epoch instance. |
| 258 | + */ |
| 259 | + normalized(): Epoch { |
| 260 | + let { integer, numerator, denominator } = this; |
| 261 | + |
| 262 | + // Normalize negative numerator values by borrowing from the whole integer. |
| 263 | + if (numerator < Zero) { |
| 264 | + // Calculate how many whole units to borrow. |
| 265 | + const n = (-numerator + denominator - 1n) / denominator; |
| 266 | + integer -= n; |
| 267 | + numerator += denominator * n; |
| 268 | + } |
| 269 | + |
| 270 | + // Reduce the fraction (numerator / denominator) to its simplest form using the greatest common divisor. |
| 271 | + const g = gcd(numerator, denominator); |
| 272 | + numerator /= g; |
| 273 | + denominator /= g; |
| 274 | + |
| 275 | + // Add any whole integer overflow from the fraction. |
| 276 | + integer += numerator / denominator; |
| 277 | + |
| 278 | + // Calculate the leftover numerator after accounting for the whole integer part from the fraction. |
| 279 | + numerator %= denominator; |
| 280 | + |
| 281 | + return new Epoch(integer, numerator, denominator); |
| 282 | + } |
| 283 | + |
| 284 | + /** |
| 285 | + * Add another epoch to this one. |
| 286 | + * |
| 287 | + * If denominators differ, the method aligns to a common denominator before |
| 288 | + * adding the fractional numerators, then returns a normalized Epoch. |
| 289 | + * |
| 290 | + * @param other - EpochLike to add. |
| 291 | + * @returns New Epoch equal to this + other. |
| 292 | + */ |
| 293 | + add(other: EpochLike): Epoch { |
| 294 | + const o = Epoch.from(other); |
| 295 | + |
| 296 | + // Sum the whole integer parts. |
| 297 | + const integer = this.integer + o.integer; |
| 298 | + let numerator: Num; |
| 299 | + let denominator: Num; |
| 300 | + |
| 301 | + // If the epochs have different denominators, align them to a common denominator. |
| 302 | + if (this.denominator !== o.denominator) { |
| 303 | + numerator = |
| 304 | + o.numerator * this.denominator + this.numerator * o.denominator; |
| 305 | + denominator = this.denominator * o.denominator; |
| 306 | + } else { |
| 307 | + // If denominators are equal, simply add the numerators. |
| 308 | + numerator = this.numerator + o.numerator; |
| 309 | + denominator = this.denominator; |
| 310 | + } |
| 311 | + |
| 312 | + return new Epoch(integer, numerator, denominator).normalized(); |
| 313 | + } |
| 314 | + |
| 315 | + /** |
| 316 | + * Subtract an epoch from this epoch. |
| 317 | + * |
| 318 | + * @param other - EpochLike to subtract. |
| 319 | + * @returns New Epoch equal to this - other. |
| 320 | + */ |
| 321 | + sub(other: EpochLike): Epoch { |
| 322 | + const { integer, numerator, denominator } = Epoch.from(other); |
| 323 | + return this.add(new Epoch(-integer, -numerator, denominator)); |
| 324 | + } |
| 325 | + |
| 326 | + /** |
| 327 | + * Convert this epoch to an estimated Unix timestamp in milliseconds using as reference the block header. |
| 328 | + * |
| 329 | + * @param reference - ClientBlockHeader providing a reference epoch and timestamp. |
| 330 | + * @returns Unix timestamp in milliseconds as bigint. |
| 331 | + */ |
| 332 | + toUnix(reference: ClientBlockHeader): bigint { |
| 333 | + // Calculate the difference between the provided epoch and the reference epoch. |
| 334 | + const { integer, numerator, denominator } = this.sub(reference.epoch); |
| 335 | + |
| 336 | + return ( |
| 337 | + reference.timestamp + |
| 338 | + EPOCH_IN_MILLISECONDS * integer + |
| 339 | + (EPOCH_IN_MILLISECONDS * numerator) / denominator |
| 340 | + ); |
| 341 | + } |
| 342 | +} |
| 343 | + |
| 344 | +/** |
| 345 | + * A constant representing the epoch duration in milliseconds. |
| 346 | + * |
| 347 | + * Calculated as 4 hours in milliseconds: |
| 348 | + * 4 hours * 60 minutes per hour * 60 seconds per minute * 1000 milliseconds per second. |
| 349 | + */ |
| 350 | +const EPOCH_IN_MILLISECONDS = numFrom(4 * 60 * 60 * 1000); |
0 commit comments