Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: a helper to implement ccc base class #81

Closed
wants to merge 12 commits into from
4 changes: 4 additions & 0 deletions .github/workflows/check.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,9 @@ jobs:
run: pnpm install
- name: Build
run: pnpm build:all
- name: Test
run: |
echo 'process.env.PRIVATE_KEY = "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee";' > .env.js
pnpm test:unit
- name: Lint
run: pnpm lint
16 changes: 14 additions & 2 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,20 @@
module.exports = {
preset: "ts-jest",
testEnvironment: "node",
setupFiles: ["<rootDir>/.env.js"],
testMatch: ["<rootDir>/packages/*/**/*.test.ts"],
transform: {
"^.+.[jt]s$": [
"ts-jest",
{
tsconfig: "tsconfig.json",
useESM: true,
isolatedModules: true,
},
],
},
extensionsToTreatAsEsm: [".ts"],
moduleNameMapper: {
"^(\\.\\.?\\/.+)\\.js$": "$1",
"^(\\.{1,2}/.*)\\.js$": "$1",
},
setupFilesAfterEnv: ["<rootDir>/jest.setup.ts", "<rootDir>/.env.js"],
};
4 changes: 4 additions & 0 deletions jest.setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { jest } from '@jest/globals';

// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
global.jest = jest;
9 changes: 6 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
"packages/*"
],
"scripts": {
"test": "jest",
"test": "cross-env NODE_OPTIONS='--experimental-vm-modules --no-warnings' jest",
"test:unit": "cross-env NODE_OPTIONS='--experimental-vm-modules --no-warnings' jest --testPathIgnorePatterns=__examples__",
"test:cov": "jest --coverage",
"build:prepare": "pnpm -r --filter !./packages/demo --filter !./packages/faucet --filter !. install",
"build": "pnpm -r --filter !./packages/demo --filter !./packages/faucet run build",
Expand All @@ -20,9 +21,11 @@
"devDependencies": {
"@changesets/changelog-github": "^0.5.0",
"@changesets/cli": "^2.27.7",
"@jest/globals": "^29.7.0",
"@types/jest": "^29.5.12",
"jest": "30.0.0-alpha.6",
"ts-jest": "^29.2.5",
"cross-env": "^7.0.3",
"jest": "^29.7.0",
"ts-jest": "^29.1.4",
"typedoc": "^0.26.6",
"typedoc-material-theme": "^1.1.0",
"typedoc-plugin-extras": "^3.1.0",
Expand Down
34 changes: 34 additions & 0 deletions packages/core/src/bytes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,3 +118,37 @@ export function bytesFrom(
}
return new Uint8Array(bytes);
}

/**
* Compares two byte-like values for equality.
* @public
*
* @param a - The first byte-like value to compare.
* @param b - The second byte-like value to compare.
* @returns A boolean indicating whether the two byte-like values are equal.
*
* @example
* ```typescript
* bytesEqual([1], Uint8Array.from([1])) // true
* ```
*/
export function bytesEqual(a: BytesLike, b: BytesLike): boolean {
if (a === b) {
return true;
}

const x = bytesFrom(a);
const y = bytesFrom(b);

if (x.length !== y.length) {
return false;
}

for (let i = 0; i < x.length; i++) {
if (x[i] !== y[i]) {
return false;
}
}

return true;
}
48 changes: 48 additions & 0 deletions packages/core/src/ckb/base.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { randomBytes } from "node:crypto";
import { bytesEqual, bytesFrom } from "../barrel.js";
import { HashType } from "../ckb/script.js";
import { Hex, hexFrom } from "../hex/index.js";
import { Base, extendsBase } from "./base.js";

test("base", () => {
class Script extends Base {
constructor(
public codeHash: Hex,
public hashType: HashType,
public args: Hex,
) {
super();
}
}

const codeHash = hexFrom(randomBytes(32));
const args = hexFrom(randomBytes(32));
const bytesToConstructorParams = jest.fn(
(): ConstructorParameters<typeof Script> => [codeHash, "type", args],
);

const scriptBytes = hexFrom(randomBytes(8));
const instanceToBytes = jest.fn(() => scriptBytes);

const CCCScript = extendsBase(
Script,
bytesToConstructorParams,
instanceToBytes,
);

expect(CCCScript.name).toBe("Script");

const script1 = CCCScript.fromBytes(scriptBytes);
const script2 = new CCCScript(codeHash, "type", args);

expect(script1).toBeInstanceOf(CCCScript);
expect(bytesToConstructorParams).toHaveBeenCalledWith(bytesFrom(scriptBytes));

expect(script2.eq(script1)).toBe(true);
expect(script2.hash()).toMatch(/^0x/);

expect(bytesEqual(script1.toBytes(), script2.toBytes())).toBe(true);

expect(script1.clone()).toEqual(script1);
expect(script1.clone()).not.toBe(script1);
});
76 changes: 76 additions & 0 deletions packages/core/src/ckb/base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { Bytes, bytesEqual, bytesFrom, BytesLike } from "../bytes/index.js";
import { hashCkb } from "../hasher/index.js";
import { Hex } from "../hex/index.js";

/**
* The base class of CCC to create a serializable instance
* @public
*/
export class Base {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
static fromBytes<T extends Base>(bytes: BytesLike): T {
throw new Error(
`"${this.name}" has to implement the static "fromBytes" method`,
);
}

toBytes(): Bytes {
throw new Error(
`"${this.constructor.name}" has to implement the "toBytes" method`,
);
}

clone(): this {
return (this.constructor as typeof Base).fromBytes(this.toBytes());
}

eq(other: this): boolean {
return bytesEqual(this.toBytes(), other.toBytes());
}

hash(): Hex {
return hashCkb(this.toBytes());
}
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
type Constructor<Instance = any, Args extends any[] = any[]> = new (
...args: Args
) => Instance;

/**
* A helper function to implement the "fromBytes" and "toBytes" method on the {@link Base} class
* @example
* ```typescript
* extendsBase(
* class Script extends Base {
* constructor(public codeHash: Hex, public hashType: HashType, public args: Hex) {}
* }
* bytesToConstructorParams,
* scriptToBytes,
* )
* ```
*/
export function extendsBase<
C extends Constructor<Base>,
const P extends ConstructorParameters<C> = ConstructorParameters<C>,
>(
FromBase: C,
bytesToConstructorParams: (bytes: Uint8Array) => P,
instanceToBytes: (instance: InstanceType<C>) => BytesLike,
): { fromBytes(bytes: BytesLike): InstanceType<C> } & C {
class Impl extends FromBase {
static fromBytes(bytesLike: BytesLike): InstanceType<C> {
const params = bytesToConstructorParams(bytesFrom(bytesLike));
return new Impl(...params) as InstanceType<C>;
}

toBytes() {
return bytesFrom(instanceToBytes(this as InstanceType<C>));
}
}

Object.defineProperty(Impl, "name", { value: FromBase.name });

return Impl;
}
1 change: 1 addition & 0 deletions packages/core/src/ckb/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from "./base.js";
export * from "./script.js";
export * from "./transaction.js";
export * from "./transactionLumos.js";
33 changes: 33 additions & 0 deletions packages/lumos-patches/src/codec.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Base, extendsBase, HashType, Hex } from "@ckb-ccc/core";
import { blockchain } from "@ckb-lumos/base";

class Script extends Base {
constructor(
public codeHash: Hex,
public hashType: HashType,
public args: Hex,
) {
super();
}
}

const ScriptCCCModel = extendsBase(
Script,
(bytes) => {
const script = blockchain.Script.unpack(bytes);
return [script.codeHash as Hex, script.hashType, script.args as Hex];
},
(instance) => blockchain.Script.pack(instance),
);

test("simple extendsBase with codec", () => {
const script = new ScriptCCCModel(
"0x0000000000000000000000000000000000000000000000000000000000000000",
"type",
"0x",
);

const bytes = script.toBytes();

expect(ScriptCCCModel.fromBytes(bytes).eq(script)).toBe(true);
});
Loading