diff --git a/.changeset/crazy-hairs-greet.md b/.changeset/crazy-hairs-greet.md new file mode 100644 index 000000000..5ba3e63e3 --- /dev/null +++ b/.changeset/crazy-hairs-greet.md @@ -0,0 +1,5 @@ +--- +"@ckb-ccc/core": minor +--- + +feat(Epoch): transform `Epoch` into a class and add utilities diff --git a/.changeset/fair-items-shout.md b/.changeset/fair-items-shout.md new file mode 100644 index 000000000..e0da4d468 --- /dev/null +++ b/.changeset/fair-items-shout.md @@ -0,0 +1,6 @@ +--- +"@ckb-ccc/core": minor +--- + +feat(core): `mol.padding` for padding codec + \ No newline at end of file diff --git a/packages/core/src/ckb/epoch.test.ts b/packages/core/src/ckb/epoch.test.ts new file mode 100644 index 000000000..e969db408 --- /dev/null +++ b/packages/core/src/ckb/epoch.test.ts @@ -0,0 +1,241 @@ +import { describe, expect, it } from "vitest"; +import type { ClientBlockHeader } from "../client/index.js"; +import { Epoch, epochFrom, epochFromHex, epochToHex } from "./epoch"; + +describe("Epoch", () => { + it("constructs from tuple and object via from()", () => { + const a = Epoch.from([1n, 2n, 3n]); + expect(a.integer).toBe(1n); + expect(a.numerator).toBe(2n); + expect(a.denominator).toBe(3n); + + const b = Epoch.from({ integer: 4n, numerator: 5n, denominator: 6n }); + expect(b.integer).toBe(4n); + expect(b.numerator).toBe(5n); + expect(b.denominator).toBe(6n); + + const c = new Epoch(7n, 8n, 9n); + expect(Epoch.from(c)).toBe(c); + }); + + it("packs and unpacks numeric layout (toNum/fromNum) and hex conversion", () => { + const e = new Epoch(0x010203n, 0x0405n, 0x0607n); // use values within bit widths + const packed1 = e.toNum(); + // integer in lower 24 bits, numerator next 16, denominator next 16 + expect(packed1 & 0xffffffn).toBe(0x010203n); + expect((packed1 >> 24n) & 0xffffn).toBe(0x0405n); + expect((packed1 >> 40n) & 0xffffn).toBe(0x0607n); + + const hex = e.toPackedHex(); + expect(typeof hex).toBe("string"); + expect(hex.startsWith("0x")).toBe(true); + + // round-trip + const decoded = Epoch.fromNum(packed1); + expect(decoded.integer).toBe(e.integer); + expect(decoded.numerator).toBe(e.numerator); + expect(decoded.denominator).toBe(e.denominator); + }); + + it("throws when packing negative components with toNum", () => { + const e = new Epoch(-1n, 0n, 1n); + expect(() => e.toNum()).toThrow(); + + const e2 = new Epoch(0n, -1n, 1n); + expect(() => e2.toNum()).toThrow(); + + const e3 = new Epoch(0n, 0n, -1n); + expect(() => e3.toNum()).toThrow(); + }); + + it("throws when packing components too big with toNum", () => { + const e = new Epoch(1n << 24n, 1n, 1n); // integer = 16777215 (24-bit limit + 1) + expect(() => e.toNum()).toThrow(); + + const e2 = new Epoch(1n, 1n << 16n, 1n); // numerator = 65536 (16-bit limit + 1) + expect(() => e2.toNum()).toThrow(); + + const e3 = new Epoch(1n, 1n, 1n << 16n); // denominator = 65536 (16-bit limit + 1) + expect(() => e3.toNum()).toThrow(); + }); + + it("normalizeBase fixes zero or negative denominators", () => { + const a = new Epoch(1n, 2n, 0n).normalizeBase(); + expect(a.denominator).toBe(1n); + expect(a.numerator).toBe(0n); + + const b = new Epoch(1n, 2n, -3n).normalizeBase(); + expect(b.denominator).toBe(3n); + expect(b.numerator).toBe(-2n); + }); + + it("normalizeCanonical reduces fractions and carries/borrows correctly", () => { + // reduction by gcd: 2/4 -> 1/2 + const a = new Epoch(1n, 2n, 4n).normalizeCanonical(); + expect(a.integer).toBe(1n); + expect(a.numerator).toBe(1n); + expect(a.denominator).toBe(2n); + + // carry: 5/2 -> +2 integer, remainder 1/2 + const b = new Epoch(0n, 5n, 2n).normalizeCanonical(); + expect(b.integer).toBe(2n); + expect(b.numerator).toBe(1n); + expect(b.denominator).toBe(2n); + + // borrow when numerator negative + const c = new Epoch(5n, -1n, 2n).normalizeCanonical(); + // -1/2 borrowed: integer 4, numerator becomes 1/2 + expect(c.integer).toBe(4n); + expect(c.numerator).toBe(1n); + expect(c.denominator).toBe(2n); + }); + + it("clone returns a deep copy", () => { + const e = new Epoch(1n, 1n, 1n); + const c = e.clone(); + expect(c).not.toBe(e); + expect(c.integer).toBe(e.integer); + expect(c.numerator).toBe(e.numerator); + expect(c.denominator).toBe(e.denominator); + }); + + it("Genesis and OneNervosDaoCycle helpers", () => { + const g = Epoch.Genesis; + expect(g.integer).toBe(0n); + expect(g.numerator).toBe(0n); + expect(g.denominator).toBe(0n); + + const o = Epoch.OneNervosDaoCycle; + expect(o.integer).toBe(180n); + expect(o.numerator).toBe(0n); + expect(o.denominator).toBe(1n); + }); + + it("comparison operations and compare()", () => { + const a = new Epoch(1n, 0n, 1n); + const b = new Epoch(1n, 1n, 2n); + const c = new Epoch(2n, 0n, 1n); + + expect(a.compare(b)).toBe(-1); + expect(b.compare(a)).toBe(1); + expect(a.compare(a)).toBe(0); + + expect(a.lt(b)).toBe(true); + expect(b.le(b)).toBe(true); + expect(b.eq(new Epoch(1n, 2n, 4n))).toBe(true); // 1 + 1/2 == 1 + 2/4 + expect(c.gt(b)).toBe(true); + expect(c.ge(b)).toBe(true); + }); + + it("add and sub arithmetic with differing denominators", () => { + const a = new Epoch(1n, 1n, 2n); // 1.5 + const b = new Epoch(2n, 1n, 3n); // 2 + 1/3 + const s = a.add(b); + // compute expected: whole = 3, fractional = 1/2 + 1/3 = 5/6 -> 3 + 5/6 + expect(s.integer).toBe(3n); + expect(s.numerator).toBe(5n); + expect(s.denominator).toBe(6n); + + const sub = s.sub(new Epoch(1n, 5n, 6n)); + expect(sub.integer).toBe(2n); + expect(sub.numerator).toBe(0n); + expect(sub.denominator).toBe(1n); + }); + + it("toUnix estimates timestamp using a reference header", () => { + const refEpoch = new Epoch(1n, 0n, 1n); + // Provide a minimal shaped header for toUnix without using `any`. + const refHeader = { + epoch: refEpoch, + timestamp: 1000n, + }; + + // target epoch is 2 + 1/2 + const target = new Epoch(2n, 1n, 2n); + const delta = target.sub(refEpoch); // should be 1 + 1/2 + + // Test default behavior (4 hours) + const expectedDefault = + refHeader.timestamp + + DEFAULT_EPOCH_IN_MILLISECONDS * delta.integer + + (DEFAULT_EPOCH_IN_MILLISECONDS * delta.numerator) / delta.denominator; + + expect(target.toUnix(refHeader)).toBe(expectedDefault); + + // Test custom epoch duration (10 minutes) + const customEpochMs = 10n * 60n * 1000n; + const expectedCustom = + refHeader.timestamp + + customEpochMs * delta.integer + + (customEpochMs * delta.numerator) / delta.denominator; + + expect(target.toUnix(refHeader, customEpochMs)).toBe(expectedCustom); + }); + + it("toUnix accepts full ClientBlockHeader", () => { + const refEpoch = new Epoch(1n, 0n, 1n); + // Simulate a full ClientBlockHeader object + const fullHeader: ClientBlockHeader = { + epoch: refEpoch, + timestamp: 1000n, + compactTarget: 0n, + hash: "0x1234567890abcdef", + number: 100n, + parentHash: "0xabcdef1234567890", + version: 0n, + nonce: 0n, + dao: { c: 0n, ar: 0n, s: 0n, u: 0n }, + extraHash: "0x0000000000000000", + proposalsHash: "0x0000000000000000", + transactionsRoot: "0x0000000000000000", + }; + + const target = new Epoch(2n, 1n, 2n); + const delta = target.sub(fullHeader.epoch); + const expected = + fullHeader.timestamp + + DEFAULT_EPOCH_IN_MILLISECONDS * delta.integer + + (DEFAULT_EPOCH_IN_MILLISECONDS * delta.numerator) / delta.denominator; + + // Full ClientBlockHeader should work due to structural typing + expect(target.toUnix(fullHeader)).toBe(expected); + }); + + it("toUnix accepts object literal with exact required properties", () => { + const target = new Epoch(2n, 1n, 2n); + const minimalRef = { + epoch: new Epoch(1n, 0n, 1n), + timestamp: 1000n, + }; + + const delta = target.sub(minimalRef.epoch); + const expected = + minimalRef.timestamp + + DEFAULT_EPOCH_IN_MILLISECONDS * delta.integer + + (DEFAULT_EPOCH_IN_MILLISECONDS * delta.numerator) / delta.denominator; + + expect(target.toUnix(minimalRef)).toBe(expected); + }); + + it("deprecated helpers epochFrom / epochFromHex / epochToHex", () => { + const e = new Epoch(3n, 4n, 5n); + expect(epochFrom(e)).toBe(e); + + const hex = epochToHex(e); + expect(typeof hex).toBe("string"); + expect(hex.startsWith("0x")).toBe(true); + + const decoded = epochFromHex(hex); + expect(decoded.integer).toBe(e.integer); + expect(decoded.numerator).toBe(e.numerator); + expect(decoded.denominator).toBe(e.denominator); + }); +}); + +/** + * DEFAULT_EPOCH_IN_MILLISECONDS + * + * Constant duration of a single standard ideal epoch expressed in milliseconds. + * Defined as 4 hours = 4 * 60 * 60 * 1000 ms. + */ +const DEFAULT_EPOCH_IN_MILLISECONDS = 4n * 60n * 60n * 1000n; diff --git a/packages/core/src/ckb/epoch.ts b/packages/core/src/ckb/epoch.ts new file mode 100644 index 000000000..36185a056 --- /dev/null +++ b/packages/core/src/ckb/epoch.ts @@ -0,0 +1,488 @@ +import type { ClientBlockHeader } from "../client/clientTypes.js"; +import { Zero } from "../fixedPoint/index.js"; +import { type Hex, type HexLike } from "../hex/index.js"; +import { mol } from "../molecule/index.js"; +import { numFrom, NumLike, numToHex, type Num } from "../num/index.js"; +import { gcd } from "../utils/index.js"; + +/** + * EpochLike + * + * Union type that represents any allowed input shapes that can be converted + * into an Epoch instance. + * + * Accepted shapes: + * - Tuple: [integer, numerator, denominator] where each element is NumLike + * - Object: { integer, numerator, denominator } where each field is NumLike + * - Packed numeric form: Num (bigint) or Hex (RPC-style packed hex) + * + * Notes: + * - When constructing an Epoch from a Num or Hex the packed numeric representation + * encodes integer (24 bits), numerator (16 bits) and denominator (16 bits). + * - Use Epoch.from() to convert any EpochLike into an Epoch instance. + * + * @example + * // From tuple + * Epoch.from([1n, 0n, 1n]); + */ +export type EpochLike = + | [NumLike, NumLike, NumLike] + | { + integer: NumLike; + numerator: NumLike; + denominator: NumLike; + } + | Num + | Hex; + +/** + * Epoch + * + * Represents a blockchain epoch consisting of a whole integer part and an + * optional fractional part represented as numerator/denominator. + * + * Behavior highlights: + * - Internally stores values as Num (bigint). + * - Provides normalization routines to canonicalize the fractional part: + * - normalizeBase(): fixes zero/negative denominators + * - normalizeCanonical(): reduces fraction, borrows/carries whole units + * - Supports arithmetic (add/sub), comparison and conversion utilities. + * + * @example + * const e = new Epoch(1n, 1n, 2n); // 1 + 1/2 + * + * @remarks + * This class is primarily a thin value-object; operations return new Epoch instances. + */ +@mol.codec( + mol.struct({ + padding: mol.padding(1), + denominator: mol.uint(2), + numerator: mol.uint(2), + integer: mol.uint(3), + }), +) +export class Epoch extends mol.Entity.Base() { + /** + * Construct a new Epoch instance. + * + * @param integer - Whole epoch units (Num/bigint) + * @param numerator - Fractional numerator (Num). + * @param denominator - Fractional denominator (Num). + */ + public constructor( + public readonly integer: Num, + public readonly numerator: Num, + public readonly denominator: Num, + ) { + super(); + } + + /** + * Normalize simpler base invariants: + * - If denominator === 0, set denominator to 1 and numerator to 0 for arithmetic convenience. + * - If denominator is negative flip signs of numerator and denominator to keep denominator positive. + * + * This is a minimal correction used before arithmetic or canonical normalization. + * + * @returns New Epoch with denominator corrected (but fraction not reduced). + */ + normalizeBase(): Epoch { + if (this.denominator === Zero) { + return new Epoch(this.integer, Zero, numFrom(1)); + } + + if (this.denominator < Zero) { + return new Epoch(this.integer, -this.numerator, -this.denominator); + } + + return this; + } + + /** + * Perform full canonical normalization of the epoch value. + * + * Steps: + * 1. Apply base normalization (normalizeBase). + * 2. If numerator is negative, borrow whole denominator(s) from the integer part + * so numerator becomes non-negative. This ensures 0 <= numerator < denominator whenever possible. + * 3. Reduce numerator/denominator by their greatest common divisor (gcd). + * 4. Carry any whole units from the reduced numerator into the integer part. + * 5. Ensure numerator is the strict remainder (numerator < denominator). + * + * @returns Canonicalized Epoch with a non-negative, reduced fractional part and integer adjusted accordingly. + */ + normalizeCanonical(): Epoch { + let { integer, numerator, denominator } = this.normalizeBase(); + + // If numerator is negative, borrow enough whole denominators from integer so numerator >= 0. + if (numerator < Zero) { + // n is the minimal non-negative integer such that numerator + n * denominator >= 0 + const n = (-numerator + denominator - 1n) / denominator; + integer -= n; + numerator += denominator * n; + } + + // Reduce the fractional part to lowest terms to keep canonical form and avoid unnecessarily large multiples. + const g = gcd(numerator, denominator); + numerator /= g; + denominator /= g; + + // Move any full units contained in the fraction into integer (e.g., 5/2 => +2 integer, remainder 1/2). + integer += numerator / denominator; + + // Remainder numerator after removing whole units; ensures numerator < denominator. + numerator %= denominator; + + return new Epoch(integer, numerator, denominator); + } + + /** + * Backwards-compatible array-style index 0 referencing the whole epoch integer. + * + * @returns integer portion (Num) + * @deprecated Use `.integer` property instead. + */ + get 0(): Num { + return this.integer; + } + + /** + * Backwards-compatible array-style index 1 referencing the epoch fractional numerator. + * + * @returns numerator portion (Num) + * @deprecated Use `.numerator` property instead. + */ + get 1(): Num { + return this.numerator; + } + + /** + * Backwards-compatible array-style index 2 referencing the epoch fractional denominator. + * + * @returns denominator portion (Num) + * @deprecated Use `.denominator` property instead. + */ + get 2(): Num { + return this.denominator; + } + + /** + * Convert this Epoch into its RPC-style packed numeric representation (Num). + * + * Packing layout (little-endian style fields): + * - integer: lower 24 bits + * - numerator: next 16 bits + * - denominator: next 16 bits + * + * Throws if any component is negative since packed representation assumes non-negative components. + * + * @throws {Error} If integer, numerator or denominator are negative. + * @throws {Error} If integer, numerator or denominator overflow the packing limits. + * @returns Packed numeric representation (Num) suitable for RPC packing. + */ + toNum(): Num { + if ( + this.integer < Zero || + this.numerator < Zero || + this.denominator < Zero + ) { + throw Error("Negative values in Epoch to Num conversion"); + } + + if ( + this.integer >= numFrom("0x1000000") || // 24-bit limit + this.numerator >= numFrom("0x10000") || // 16-bit limit + this.denominator >= numFrom("0x10000") // 16-bit limit + ) { + throw Error( + "Integer must be < 2^24, numerator and denominator must be < 2^16", + ); + } + + return ( + this.integer + + (this.numerator << numFrom(24)) + + (this.denominator << numFrom(40)) + ); + } + + /** + * Convert epoch to hex string representation of the RPC-style packed numeric form. + * + * Returns the same representation used by CKB RPC responses where the + * packed numeric bytes may be trimmed of leading zeros, see {@link numToHex} + * + * @returns Hex string corresponding to the packed epoch. + */ + toPackedHex(): Hex { + return numToHex(this.toNum()); + } + + /** + * Construct an Epoch by unpacking a RPC-style packed numeric form. + * + * @param v - NumLike packed epoch (like Num and Hex) + * @returns Epoch whose integer, numerator and denominator are extracted from the packed layout. + */ + static fromNum(v: NumLike): Epoch { + const num = numFrom(v); + + return new Epoch( + num & numFrom("0xffffff"), + (num >> numFrom(24)) & numFrom("0xffff"), + (num >> numFrom(40)) & numFrom("0xffff"), + ); + } + + /** + * Create an Epoch from an EpochLike value. + * + * Accepts: + * - an Epoch instance (returned as-is) + * - an array [integer, numerator, denominator] where each element is NumLike + * - an object { integer, numerator, denominator } where each field is NumLike + * - a packed numeric-like value handled by fromNum + * + * All numeric-like inputs are converted with numFrom() to produce internal Num values. + * + * @param e - Value convertible to Epoch + * @returns Epoch instance + */ + static override from(e: EpochLike): Epoch { + if (e instanceof Epoch) { + return e; + } + + if (Array.isArray(e)) { + return new Epoch(numFrom(e[0]), numFrom(e[1]), numFrom(e[2])); + } + + if (typeof e === "object") { + return new Epoch( + numFrom(e.integer), + numFrom(e.numerator), + numFrom(e.denominator), + ); + } + + return Epoch.fromNum(e); + } + + /** + * Return a deep copy of this Epoch. + * + * @returns New Epoch instance with identical components. + */ + override clone(): Epoch { + return new Epoch(this.integer, this.numerator, this.denominator); + } + + /** + * Return the genesis epoch. + * + * Note: for historical reasons the genesis epoch is represented with all-zero + * fields, no other epoch instance should use a zero denominator. + * + * @returns Epoch with integer = 0, numerator = 0, denominator = 0. + */ + static get Genesis(): Epoch { + return new Epoch(Zero, Zero, Zero); + } + + /** + * Return an Epoch representing one Nervos DAO cycle (180 epochs exactly). + * + * @returns Epoch equal to 180 with denominator set to 1 to represent an exact whole unit. + */ + static get OneNervosDaoCycle(): Epoch { + return new Epoch(numFrom(180), Zero, numFrom(1)); + } + + /** + * Compare this epoch to another EpochLike. + * + * The comparison computes scaled integer values so fractions are compared without precision loss: + * scaled = (integer * denominator + numerator) * other.denominator + * + * Special-case: identical object references return equality immediately. + * + * @param other - Epoch-like value to compare against. + * @returns 1 if this > other, 0 if equal, -1 if this < other. + * + * @example + * epochA.compare(epochB); // -1|0|1 + */ + compare(other: EpochLike): 1 | 0 | -1 { + if (this === other) { + return 0; + } + + const t = this.normalizeBase(); + const o = Epoch.from(other).normalizeBase(); + + // Compute scaled representations to compare fractions without floating-point arithmetic. + const a = (t.integer * t.denominator + t.numerator) * o.denominator; + const b = (o.integer * o.denominator + o.numerator) * t.denominator; + + return a > b ? 1 : a < b ? -1 : 0; + } + + /** + * Check whether this epoch is less than another EpochLike. + * + * @param other - EpochLike to compare against. + * @returns true if this < other. + */ + lt(other: EpochLike): boolean { + return this.compare(other) < 0; + } + + /** + * Check whether this epoch is less than or equal to another EpochLike. + * + * @param other - EpochLike to compare against. + * @returns true if this <= other. + */ + le(other: EpochLike): boolean { + return this.compare(other) <= 0; + } + + /** + * Check whether this epoch equals another EpochLike. + * + * @param other - EpochLike to compare against. + * @returns true if equal. + */ + eq(other: EpochLike): boolean { + return this.compare(other) === 0; + } + + /** + * Check whether this epoch is greater than or equal to another EpochLike. + * + * @param other - EpochLike to compare against. + * @returns true if this >= other. + */ + ge(other: EpochLike): boolean { + return this.compare(other) >= 0; + } + + /** + * Check whether this epoch is greater than another EpochLike. + * + * @param other - EpochLike to compare against. + * @returns true if this > other. + */ + gt(other: EpochLike): boolean { + return this.compare(other) > 0; + } + + /** + * Add another EpochLike to this epoch and return the normalized result. + * + * Rules and edge-cases: + * - Whole parts are added directly; fractional parts are aligned to a common denominator and added. + * - Final result is canonicalized to reduce the fraction and carry any overflow to the integer part. + * + * @param other - Epoch-like value to add. + * @returns Normalized Epoch representing the sum. + */ + add(other: EpochLike): Epoch { + const t = this.normalizeBase(); + const o = Epoch.from(other).normalizeBase(); + + // Sum whole integer parts. + const integer = t.integer + o.integer; + let numerator: Num; + let denominator: Num; + + // Align denominators if they differ; use multiplication to obtain a common denominator. + if (t.denominator !== o.denominator) { + // Numerators & Denominators are generally small (<= 2000n); multiplication produces a safe common denominator. + numerator = t.numerator * o.denominator + o.numerator * t.denominator; + denominator = t.denominator * o.denominator; + } else { + numerator = t.numerator + o.numerator; + denominator = t.denominator; + } + + // Normalize to reduce fraction and carry whole units into integer. + return new Epoch(integer, numerator, denominator).normalizeCanonical(); + } + + /** + * Subtract an EpochLike from this epoch and return the normalized result. + * + * Implementation notes: + * - Delegates to add by negating the other epoch's integer and numerator while preserving denominator. + * - normalizeCanonical will handle negative numerators by borrowing from integer as necessary. + * + * @param other - Epoch-like value to subtract. + * @returns Normalized Epoch representing this - other. + */ + sub(other: EpochLike): Epoch { + const { integer, numerator, denominator } = Epoch.from(other); + return this.add(new Epoch(-integer, -numerator, denominator)); + } + + /** + * Convert this epoch to an estimated Unix timestamp in milliseconds using a reference header. + * + * Note: This is an estimation that assumes a constant epoch duration. + * + * @param reference - Object providing `epoch` (Epoch) and `timestamp` (Num) fields, such as a ClientBlockHeader. + * @param epochInMilliseconds - Duration of a single epoch in milliseconds. Defaults to 4 hours. + * @returns Estimated Unix timestamp in milliseconds as bigint. + */ + toUnix( + reference: Pick, + epochInMilliseconds: Num = numFrom(4 * 60 * 60 * 1000), + ): bigint { + // Compute relative epoch difference against the reference header. + const { integer, numerator, denominator } = this.sub(reference.epoch); + + // Add whole epoch duration and fractional epoch duration to the reference timestamp. + return ( + reference.timestamp + + epochInMilliseconds * integer + + (epochInMilliseconds * numerator) / denominator + ); + } +} + +/** + * epochFrom + * + * @deprecated prefer using Epoch.from() directly. + * + * @param epochLike - Epoch-like value to convert. + * @returns Epoch instance corresponding to the input. + */ +export function epochFrom(epochLike: EpochLike): Epoch { + return Epoch.from(epochLike); +} + +/** + * epochFromHex + * + * @deprecated use Epoch.fromNum() with numeric input instead. + * + * @param hex - Hex-like or numeric-like value encoding a packed epoch. + * @returns Decoded Epoch instance. + */ +export function epochFromHex(hex: HexLike): Epoch { + return Epoch.fromNum(hex); +} + +/** + * epochToHex + * + * @deprecated use Epoch.from(epochLike).toPackedHex() instead. + * + * @param epochLike - Value convertible to an Epoch (object, tuple or Epoch). + * @returns Hex string representing the packed epoch encoding. + */ +export function epochToHex(epochLike: EpochLike): Hex { + return Epoch.from(epochLike).toPackedHex(); +} diff --git a/packages/core/src/ckb/index.ts b/packages/core/src/ckb/index.ts index 7d20b37d0..198c09299 100644 --- a/packages/core/src/ckb/index.ts +++ b/packages/core/src/ckb/index.ts @@ -1,3 +1,4 @@ +export * from "./epoch.js"; export * from "./hash.js"; export * from "./script.js"; export * from "./transaction.js"; diff --git a/packages/core/src/ckb/transaction.ts b/packages/core/src/ckb/transaction.ts index 9a5920bd9..afb901285 100644 --- a/packages/core/src/ckb/transaction.ts +++ b/packages/core/src/ckb/transaction.ts @@ -21,6 +21,7 @@ import { } from "../num/index.js"; import type { Signer } from "../signer/index.js"; import { apply, reduceAsync } from "../utils/index.js"; +import { Epoch } from "./epoch.js"; import { Script, ScriptLike, ScriptOpt } from "./script.js"; import { DEP_TYPE_TO_NUM, NUM_TO_DEP_TYPE } from "./transaction.advanced.js"; import { @@ -647,45 +648,6 @@ export class Cell extends CellAny { } } -/** - * @public - */ -export type EpochLike = [NumLike, NumLike, NumLike]; -/** - * @public - */ -export type Epoch = [Num, Num, Num]; -/** - * @public - */ -export function epochFrom(epochLike: EpochLike): Epoch { - return [numFrom(epochLike[0]), numFrom(epochLike[1]), numFrom(epochLike[2])]; -} -/** - * @public - */ -export function epochFromHex(hex: HexLike): Epoch { - const num = numFrom(hexFrom(hex)); - - return [ - num & numFrom("0xffffff"), - (num >> numFrom(24)) & numFrom("0xffff"), - (num >> numFrom(40)) & numFrom("0xffff"), - ]; -} -/** - * @public - */ -export function epochToHex(epochLike: EpochLike): Hex { - const epoch = epochFrom(epochLike); - - return numToHex( - numFrom(epoch[0]) + - (numFrom(epoch[1]) << numFrom(24)) + - (numFrom(epoch[2]) << numFrom(40)), - ); -} - /** * @public */ @@ -1887,7 +1849,7 @@ export class Transaction extends mol.Entity.Base< return reduceAsync( this.inputs, async (acc, input) => acc + (await input.getExtraCapacity(client)), - numFrom(0), + Zero, ); } @@ -1903,16 +1865,13 @@ export class Transaction extends mol.Entity.Base< return acc + capacity; }, - numFrom(0), + Zero, )) + (await this.getInputsCapacityExtra(client)) ); } getOutputsCapacity(): Num { - return this.outputs.reduce( - (acc, { capacity }) => acc + capacity, - numFrom(0), - ); + return this.outputs.reduce((acc, { capacity }) => acc + capacity, Zero); } async getInputsUdtBalance(client: Client, type: ScriptLike): Promise { @@ -1926,7 +1885,7 @@ export class Transaction extends mol.Entity.Base< return acc + udtBalanceFrom(outputData); }, - numFrom(0), + Zero, ); } @@ -1937,7 +1896,7 @@ export class Transaction extends mol.Entity.Base< } return acc + udtBalanceFrom(this.outputsData[i]); - }, numFrom(0)); + }, Zero); } async completeInputs( @@ -2059,7 +2018,7 @@ export class Transaction extends mol.Entity.Base< ): Promise { const expectedBalance = this.getOutputsUdtBalance(type) + numFrom(balanceTweak ?? 0); - if (expectedBalance === numFrom(0)) { + if (expectedBalance === Zero) { return 0; } @@ -2073,7 +2032,7 @@ export class Transaction extends mol.Entity.Base< return [balanceAcc + udtBalanceFrom(outputData), countAcc + 1]; }, - [numFrom(0), 0], + [Zero, 0], ); if ( @@ -2512,12 +2471,12 @@ export function calcDaoProfit( * * @param depositHeader - The block header when the DAO deposit was made. * @param withdrawHeader - The block header when the DAO withdrawal was initiated. - * @returns The epoch when the withdrawal can be claimed, represented as [number, index, length]. + * @returns The epoch when the withdrawal can be claimed, represented as an Epoch instance. * * @example * ```typescript - * const claimEpoch = calcDaoClaimEpoch(depositHeader, withdrawHeader); - * console.log(`Can claim at epoch: ${claimEpoch[0]}, index: ${claimEpoch[1]}, length: ${claimEpoch[2]}`); + * const epoch = calcDaoClaimEpoch(depositHeader, withdrawHeader); + * console.log(`Can claim at epoch: ${epoch.integer}, numerator: ${epoch.numerator}, denominator: ${epoch.denominator}`); * ``` * * @remarks @@ -2531,26 +2490,23 @@ export function calcDaoClaimEpoch( depositHeader: ClientBlockHeaderLike, withdrawHeader: ClientBlockHeaderLike, ): Epoch { - const depositEpoch = ClientBlockHeader.from(depositHeader).epoch; - const withdrawEpoch = ClientBlockHeader.from(withdrawHeader).epoch; - const intDiff = withdrawEpoch[0] - depositEpoch[0]; - // deposit[1] withdraw[1] - // ---------- <= ----------- - // deposit[2] withdraw[2] + const deposit = ClientBlockHeader.from(depositHeader).epoch.normalizeBase(); + const withdraw = ClientBlockHeader.from(withdrawHeader).epoch.normalizeBase(); + + const fullCycle = numFrom(180); + const partialCycle = (withdraw.integer - deposit.integer) % fullCycle; + let withdrawInteger = withdraw.integer; if ( - intDiff % numFrom(180) !== numFrom(0) || - depositEpoch[1] * withdrawEpoch[2] <= depositEpoch[2] * withdrawEpoch[1] + partialCycle !== Zero || + // deposit.numerator withdraw.numerator + // --------------------- <= ---------------------- + // deposit.denominator withdraw.denominator + deposit.numerator * withdraw.denominator <= + withdraw.numerator * deposit.denominator ) { - return [ - depositEpoch[0] + (intDiff / numFrom(180) + numFrom(1)) * numFrom(180), - depositEpoch[1], - depositEpoch[2], - ]; + // Need to wait for the next cycle + withdrawInteger += -partialCycle + fullCycle; } - return [ - depositEpoch[0] + (intDiff / numFrom(180)) * numFrom(180), - depositEpoch[1], - depositEpoch[2], - ]; + return new Epoch(withdrawInteger, deposit.numerator, deposit.denominator); } diff --git a/packages/core/src/client/clientTypes.ts b/packages/core/src/client/clientTypes.ts index f7331e8d2..ec57a1a12 100644 --- a/packages/core/src/client/clientTypes.ts +++ b/packages/core/src/client/clientTypes.ts @@ -12,7 +12,6 @@ import { ScriptLike, Transaction, TransactionLike, - epochFrom, hashTypeFrom, } from "../ckb/index.js"; import { Hex, HexLike, hexFrom } from "../hex/index.js"; @@ -398,7 +397,7 @@ export class ClientBlockHeader { s: numFrom(headerLike.dao.s), u: numFrom(headerLike.dao.u), }, - epochFrom(headerLike.epoch), + Epoch.from(headerLike.epoch), hexFrom(headerLike.extraHash), hexFrom(headerLike.hash), numFrom(headerLike.nonce), diff --git a/packages/core/src/client/jsonRpc/transformers.ts b/packages/core/src/client/jsonRpc/transformers.ts index ded06bd80..a571fa412 100644 --- a/packages/core/src/client/jsonRpc/transformers.ts +++ b/packages/core/src/client/jsonRpc/transformers.ts @@ -9,6 +9,7 @@ import { CellOutputLike, DepType, DepTypeLike, + Epoch, HashType, HashTypeLike, OutPoint, @@ -18,7 +19,6 @@ import { Transaction, TransactionLike, depTypeFrom, - epochFromHex, hashTypeFrom, } from "../../ckb/index.js"; import { Hex, HexLike, hexFrom } from "../../hex/index.js"; @@ -217,7 +217,7 @@ export class JsonRpcTransformers { s: numLeFromBytes(dao.slice(16, 24)), u: numLeFromBytes(dao.slice(24, 32)), }, - epoch: epochFromHex(header.epoch), + epoch: Epoch.fromNum(header.epoch), extraHash: header.extra_hash, hash: header.hash, nonce: numFrom(header.nonce), diff --git a/packages/core/src/molecule/codec.ts b/packages/core/src/molecule/codec.ts index 4e80d7a1d..70af71faa 100644 --- a/packages/core/src/molecule/codec.ts +++ b/packages/core/src/molecule/codec.ts @@ -718,3 +718,20 @@ export function uintNumber( outMap: (num) => Number(num), }); } + +/** + * Create a codec for padding bytes. + * The padding bytes are zero-filled when encoding and ignored when decoding. + * @param byteLength The length of the padding in bytes. + */ +export function padding( + byteLength: number, +): Codec { + return Codec.from({ + byteLength, + encode: () => { + return new Uint8Array(byteLength); + }, + decode: () => {}, + }); +} diff --git a/packages/core/src/molecule/entity.ts b/packages/core/src/molecule/entity.ts index 5e1771e8c..67d91b83c 100644 --- a/packages/core/src/molecule/entity.ts +++ b/packages/core/src/molecule/entity.ts @@ -1,6 +1,6 @@ import { Bytes, bytesEq, BytesLike } from "../bytes/index.js"; import { hashCkb } from "../hasher/index.js"; -import { Hex } from "../hex/index.js"; +import { Hex, hexFrom } from "../hex/index.js"; import { Constructor } from "../utils/index.js"; import { Codec } from "./codec.js"; @@ -126,6 +126,15 @@ export abstract class Entity { hash(): Hex { return hashCkb(this.toBytes()); } + + /** + * Convert the entity to a full-byte untrimmed Hex representation + * @public + * @returns The entity full-byte untrimmed hexadecimal representation + */ + toHex(): Hex { + return hexFrom(this.toBytes()); + } } return Impl; @@ -133,6 +142,7 @@ export abstract class Entity { abstract toBytes(): Bytes; abstract hash(): Hex; + abstract toHex(): Hex; abstract clone(): Entity; } diff --git a/packages/core/src/utils/index.ts b/packages/core/src/utils/index.ts index e9e73364b..f75bdb8ab 100644 --- a/packages/core/src/utils/index.ts +++ b/packages/core/src/utils/index.ts @@ -1,4 +1,5 @@ -import { NumLike, numFrom, numToHex } from "../num/index.js"; +import { Zero } from "../fixedPoint/index.js"; +import { NumLike, numFrom, numToHex, type Num } from "../num/index.js"; /** * A type safe way to apply a transformer on a value if it's not empty. @@ -196,3 +197,21 @@ export function stringify(val: unknown) { return value; }); } + +/** + * Calculate the greatest common divisor (GCD) of two NumLike values using the Euclidean algorithm. + * + * @param a - First operand. + * @param b - Second operand. + * @returns GCD(a, b) as a Num. + */ +export function gcd(a: NumLike, b: NumLike): Num { + a = numFrom(a); + b = numFrom(b); + a = a < Zero ? -a : a; + b = b < Zero ? -b : b; + while (b !== Zero) { + [a, b] = [b, a % b]; + } + return a; +}