Skip to content

Commit c0faaf7

Browse files
committed
feat(Epoch): transform Epoch into a class and add utilities
1 parent 78f8588 commit c0faaf7

File tree

9 files changed

+422
-77
lines changed

9 files changed

+422
-77
lines changed

.changeset/crazy-hairs-greet.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@ckb-ccc/core": minor
3+
---
4+
5+
feat(Epoch): transform `Epoch` into a class and add utilities

packages/core/src/ckb/epoch.ts

Lines changed: 350 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,350 @@
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);

packages/core/src/ckb/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
export * from "./epoch.js";
12
export * from "./hash.js";
23
export * from "./script.js";
34
export * from "./transaction.js";

0 commit comments

Comments
 (0)