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(helpers): model helpers for ckb object #694

Merged
merged 5 commits into from
May 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 46 additions & 14 deletions packages/helpers/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ The difference between this and `@ckb-lumos/base`, is that `@ckb-lumos/base` con
## Usage

```javascript
const { minimalCellCapacity, generateAddress, parseAddress } = require("@ckb-lumos/helpers")
const {
minimalCellCapacity,
generateAddress,
parseAddress,
} = require("@ckb-lumos/helpers");

// Get cell minimal capacity.
const result = minimalCellCapacity({
Expand All @@ -25,7 +29,7 @@ const result = minimalCellCapacity({
blockHash: null,
blockNumber: null,
outPoint: null,
})
});

// result will be 6100000000n shannons.

Expand All @@ -35,31 +39,59 @@ const address = generateAddress({
"0x9bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8",
hashType: "type",
args: "0x36c329ed630d6ce750712a477543672adab57f4c",
})
});

// Then you will get mainnet address "ckb1qyqrdsefa43s6m882pcj53m4gdnj4k440axqdt9rtd", or you can generate testnet address by
const { predefined } = require("@ckb-lumos/config-manager")
const { predefined } = require("@ckb-lumos/config-manager");

const address = generateAddress({
codeHash:
"0x9bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8",
hashType: "type",
args: "0x36c329ed630d6ce750712a477543672adab57f4c",
}, { config: predefined.AGGRON4 })
const address = generateAddress(
{
codeHash:
"0x9bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8",
hashType: "type",
args: "0x36c329ed630d6ce750712a477543672adab57f4c",
},
{ config: predefined.AGGRON4 }
);

// Will get testnet address "ckt1qyqrdsefa43s6m882pcj53m4gdnj4k440axqswmu83".

// Use `parseAddress` to get lock script from an address.
const script = parseAddress("ckb1qyqrdsefa43s6m882pcj53m4gdnj4k440axqdt9rtd")
const script = parseAddress("ckb1qyqrdsefa43s6m882pcj53m4gdnj4k440axqdt9rtd");

// TransactionSkeleton <=> Object

// Convert TransactionSkeleton to js object
const obj = transactionSkeletonToObject(txSkeleton)
const obj = transactionSkeletonToObject(txSkeleton);
// then your can write to json file
fs.writeFileSync("your file", JSON.stringify(obj))
fs.writeFileSync("your file", JSON.stringify(obj));

// Or convert js object to TransactionSkeleton
// If your object is from json file, make sure `cellProvider` is working properly.
const txSkeleton = objectToTransactionSkeleton(obj)
const txSkeleton = objectToTransactionSkeleton(obj);
```

### ModelHelpers

The `ModelHelper` provides a set of common methods, such as `create`, `hash`, `clone`, and `equals`, for CKB-related objects.
This helper is designed to work with a `ModelLike` object for convenience, allowing developers to work with ambiguous objects instead of just the strict object.

```ts
export type ModelHelper<Model, ModelLike = Model> = {
create(modelLike: ModelLike): Model;
equals(modelLike: ModelLike, modelR: ModelLike): boolean;
hash(modelLike: ModelLike): Uint8Array;
clone(model: ModelLike): Model;
};
```

```javascript
import { cellHelper } from "@ckb-lumos/helpers";

const cell = cellHelper.create({
lock: "ckb1qzda0cr08m85hc8jlnfp3zer7xulejywt49kt2rr0vthywaa50xwsqgqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq5m759c",
});

const _61CKB = 61 * 10 ** 8;
asserts(BI.from(cell.cellOutput.capacity).eq(_61CKB)); // 61 CKB
```
1 change: 1 addition & 0 deletions packages/helpers/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -532,3 +532,4 @@ export function objectToTransactionSkeleton(
}

export { refreshTypeIdCellDeps } from "./refresh";
export * from "./models";
73 changes: 73 additions & 0 deletions packages/helpers/src/models/base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { bytes } from "@ckb-lumos/codec";
import { BytesCodec } from "@ckb-lumos/codec/lib/base";
import { ckbHash } from "@ckb-lumos/base/lib/utils";

/**
* A helper object that provides common functionalities, such as create(clone), equals, hash, etc. for models.
*/
export type ModelHelper<Model, ModelLike = Model> = {
/**
* create a Model from a ModelLike
* @param modelLike
*/
create(modelLike: ModelLike): Model;
/**
* check if the two models are equals
* @param modelLike
* @param modelR
*/
equals(modelLike: ModelLike, modelR: ModelLike): boolean;
/**
* create the hash of the model
* @param modelLike
*/
hash(modelLike: ModelLike): Uint8Array;

/**
* clone a model
* @param model
*/
clone(model: Model): Model;
};

/**
* create a {@link ModelHelper} with a {@link BytesCodec}
* @param codec
*/
export function createModelHelper<Model, ModelLike>(
codec: BytesCodec<Model, ModelLike>
): ModelHelper<Model, ModelLike> {
return {
create: (val) => codec.unpack(codec.pack(val)),
hash: (val) => bytes.bytify(ckbHash(codec.pack(val))),
equals: (a, b) => bytes.equal(codec.pack(a), codec.pack(b)),
clone: defaultDeepClone,
};
}

/**
* @internal
*/
export function defaultDeepClone<T>(value: T): T {
const valType = typeof value;

if (
valType === "number" ||
valType === "string" ||
valType === "boolean" ||
valType === "bigint" ||
value == null
) {
return value;
} else if (Array.isArray(value)) {
return value.map(defaultDeepClone) as T;
} else if (valType === "object") {
return Object.entries(value).reduce(
(result, [key, value]) =>
Object.assign(result, { [key]: defaultDeepClone(value) }),
{}
) as T;
}

throw new Error("Cannot clone the value: " + String(value));
}

Check warning on line 73 in packages/helpers/src/models/base.ts

View check run for this annotation

Codecov / codecov/patch

packages/helpers/src/models/base.ts#L72-L73

Added lines #L72 - L73 were not covered by tests
7 changes: 7 additions & 0 deletions packages/helpers/src/models/blockchain.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { blockchain } from "@ckb-lumos/base";
import { createModelHelper } from "./base";

/**
* {@link ModelHelper}
*/
export const outPointHelper = createModelHelper(blockchain.OutPoint);
67 changes: 67 additions & 0 deletions packages/helpers/src/models/cell.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { createModelHelper, ModelHelper } from "./base";
import { blockchain, Cell } from "@ckb-lumos/base";
import { scriptHelper, ScriptLike } from "./script";
import { BI, BIish } from "@ckb-lumos/bi";
import { bytes, BytesLike, PackParam } from "@ckb-lumos/codec";
import { option, table } from "@ckb-lumos/codec/lib/molecule";
import { outPointHelper } from "./blockchain";
import { minimalCellCapacityCompatible } from "../index";

type CreateCellOptions = {
capacity?: BIish;
lock: ScriptLike;
type?: PackParam<typeof blockchain.Script>;
outPoint?: PackParam<typeof blockchain.OutPoint>;
data?: BytesLike;
};

type CellLike = Cell | CreateCellOptions;

function isCreateCellOptions(val: unknown): val is CreateCellOptions {
if (!val || typeof val !== "object") return false;
return "lock" in val;
}

const CellCodec = table(
{
cellOutput: blockchain.CellOutput,
data: blockchain.Bytes,
outPoint: option(blockchain.OutPoint),
},
["cellOutput", "data", "outPoint"]
);

/**
* A set of helper functions for Cell
* @example
* const cell = CellHelper.create({ lock: 'ckb1secp256k1lock' })
* cell.cellOutput.capacity // == 61 CKB
*/
export const cellHelper: ModelHelper<Cell, CellLike> = createModelHelper({
pack: (model) => {
const cell: Cell = (() => {
if (isCreateCellOptions(model)) {
return {
cellOutput: {
capacity: BI.from(model.capacity || "0x0").toHexString(),
lock: scriptHelper.create(model.lock),
type: model.type && scriptHelper.create(model.type),
},

outPoint: model.outPoint && outPointHelper.create(model.outPoint),
data: bytes.hexify(model.data || "0x"),
};
}

return model;
})();

if (BI.from(cell.cellOutput.capacity).eq(0)) {
cell.cellOutput.capacity =
minimalCellCapacityCompatible(cell).toHexString();
}

return CellCodec.pack(cell);
},
unpack: (value) => CellCodec.unpack(value),
});
4 changes: 4 additions & 0 deletions packages/helpers/src/models/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export { type ModelHelper, createModelHelper } from "./base";
export { cellHelper } from "./cell";
export { scriptHelper } from "./script";
export { outPointHelper } from "./blockchain";
27 changes: 27 additions & 0 deletions packages/helpers/src/models/script.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { PackParam } from "@ckb-lumos/codec";
import { Address, Script, blockchain } from "@ckb-lumos/base";
import { predefined } from "@ckb-lumos/config-manager";
import { parseAddress } from "../";
import { createModelHelper } from "./base";

export type PackableScript = PackParam<typeof blockchain.Script>;
export type ScriptLike = PackableScript | Address;

function autoParseAddress(address: Address): Script {
if (address.startsWith("ckb")) {
return parseAddress(address, { config: predefined.LINA });
} else if (address.startsWith("ckt")) {
return parseAddress(address, { config: predefined.AGGRON4 });
}

throw new Error(`The address prefix ${address} is unknown`);
}

Check warning on line 18 in packages/helpers/src/models/script.ts

View check run for this annotation

Codecov / codecov/patch

packages/helpers/src/models/script.ts#L17-L18

Added lines #L17 - L18 were not covered by tests

export const scriptHelper = createModelHelper<Script, ScriptLike>({
pack: (val) =>
blockchain.Script.pack(
typeof val === "string" ? autoParseAddress(val) : val
),

unpack: (val) => blockchain.Script.unpack(val),
});
70 changes: 70 additions & 0 deletions packages/helpers/tests/model_helper.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import test from "ava";
import { cellHelper, encodeToAddress, scriptHelper } from "../src";
import { randomBytes } from "node:crypto";
import { bytes } from "@ckb-lumos/codec";
import { predefined } from "@ckb-lumos/config-manager";
import { defaultDeepClone } from "../src/models/base";
import { parseUnit } from "@ckb-lumos/bi";
import { Uint128 } from "@ckb-lumos/codec/lib/number";

test("deepClone", (t) => {
const obj = {
key1: undefined,
key2: "string",
key3: 1,
key4: 1n,
key5: true,
key6: [1, 2, 3],
key7: {
key7_01: [3, 2, 1],
},
};
t.deepEqual(defaultDeepClone(obj), obj);
});

test("scriptHelper", (t) => {
const codeHash = randomBytes(32);

const script = scriptHelper.create({
codeHash,
hashType: "type",
args: "0x",
});

t.deepEqual(script, {
codeHash: bytes.hexify(codeHash),
hashType: "type",
args: "0x",
});

const mainnetAddress = encodeToAddress(script);
t.deepEqual(scriptHelper.create(mainnetAddress), script);

const testnetAddress = encodeToAddress(script, {
config: predefined.AGGRON4,
});
t.deepEqual(scriptHelper.create(testnetAddress), script);

t.deepEqual(scriptHelper.clone(script), script);
});

test("cellHelper", (t) => {
const cell = cellHelper.create({
// fake secp256k1
lock: {
codeHash: randomBytes(32),
hashType: "type",
args: randomBytes(20),
},
// fake sudt
type: {
codeHash: randomBytes(32),
hashType: "type",
args: randomBytes(32),
},
data: Uint128.pack(0),
});

t.true(parseUnit("142", "ckb").eq(cell.cellOutput.capacity));
t.deepEqual(cellHelper.create(cell), cell);
});
8 changes: 8 additions & 0 deletions packages/lumos/src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,11 @@ export {
} from "@ckb-lumos/helpers";

export { addCellDep } from "@ckb-lumos/common-scripts/lib/helper";

export {
type ModelHelper,
createModelHelper,
cellHelper,
scriptHelper,
outPointHelper,
} from "@ckb-lumos/helpers/lib/models";
Loading
Loading