diff --git a/packages/bindings/tsconfig.json b/packages/bindings/tsconfig.json index 8f6e1b3..fd2720d 100644 --- a/packages/bindings/tsconfig.json +++ b/packages/bindings/tsconfig.json @@ -1,6 +1,8 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { + "outDir": "./dist", "types": ["./index.d.ts"] - } + }, + "include": ["src/**/*"] } diff --git a/packages/core/package.json b/packages/core/package.json index 773163c..b76ac7c 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -13,16 +13,20 @@ "sideEffects": false, "main": "./dist/index.js", "module": "./dist/index.js", + "exports": { + ".": "./dist/index.js", + "./barrel": "./dist/barrel.js" + }, "scripts": { - "build": "tsc --noEmit", + "build": "tsc", "format": "prettier --write .", "clean": "rm -f dist/*" }, - "devDependencies": {}, "publishConfig": { "access": "public" }, "dependencies": { - "@ckb-js-std/bindings": "workspace:*" + "@ckb-js-std/bindings": "workspace:*", + "@ckb-js-std/core": "link:" } } diff --git a/packages/core/src/advanced.ts b/packages/core/src/advanced.ts deleted file mode 100644 index 2ad53c4..0000000 --- a/packages/core/src/advanced.ts +++ /dev/null @@ -1 +0,0 @@ -// export * from "./advancedBarrel.js"; diff --git a/packages/core/src/advancedBarrel.ts b/packages/core/src/advancedBarrel.ts deleted file mode 100644 index b8dd0ff..0000000 --- a/packages/core/src/advancedBarrel.ts +++ /dev/null @@ -1 +0,0 @@ -// export * from "./ckb/advanced.js"; diff --git a/packages/core/src/barrel.ts b/packages/core/src/barrel.ts index 77711a2..a8eb3d6 100644 --- a/packages/core/src/barrel.ts +++ b/packages/core/src/barrel.ts @@ -1,5 +1,6 @@ -export * from "./bytes/index.js"; -export * from "./ckb/index.js"; -export * from "./molecule/index.js"; -export * from "./num/index.js"; -export * from "./utils/index.js"; +export * from "./bytes/index"; +export * from "./ckb/index"; +export * from "./molecule/index"; +export * from "./num/index"; +export * from "./utils/index"; +export * from "./hasher/index"; diff --git a/packages/core/src/bytes/index.ts b/packages/core/src/bytes/index.ts index 285754d..b76bb50 100644 --- a/packages/core/src/bytes/index.ts +++ b/packages/core/src/bytes/index.ts @@ -39,15 +39,12 @@ export function bytesEq(a: BytesLike, b: BytesLike): boolean { return true; } - const x = bytesFrom(a); - const y = bytesFrom(b); - - if (x.length !== y.length) { + if (a.length !== b.length) { return false; } - for (let i = 0; i < x.length; i++) { - if (x[i] !== y[i]) { + for (let i = 0; i < a.length; i++) { + if (a[i] !== b[i]) { return false; } } diff --git a/packages/core/src/ckb/advanced.ts b/packages/core/src/ckb/advanced.ts deleted file mode 100644 index 4196bd9..0000000 --- a/packages/core/src/ckb/advanced.ts +++ /dev/null @@ -1,2 +0,0 @@ -// export * from "./script.advanced.js"; -// export * from "./transaction.advanced"; diff --git a/packages/core/src/ckb/index.ts b/packages/core/src/ckb/index.ts index b93af5a..d71f4fc 100644 --- a/packages/core/src/ckb/index.ts +++ b/packages/core/src/ckb/index.ts @@ -1,2 +1,2 @@ export * from "./script"; -// export * from "./transaction"; +export * from "./transaction"; diff --git a/packages/core/src/ckb/script.ts b/packages/core/src/ckb/script.ts index 2ca43e8..ed2017d 100644 --- a/packages/core/src/ckb/script.ts +++ b/packages/core/src/ckb/script.ts @@ -1,5 +1,5 @@ -import { Bytes, BytesLike, bytesFrom } from "../bytes/index.js"; -import { mol } from "../molecule/index.js"; +import { Bytes, BytesLike, bytesFrom } from "../bytes/index"; +import { mol } from "../molecule/index"; export const HashTypeCodec: mol.Codec = mol.Codec.from({ byteLength: 1, diff --git a/packages/core/src/ckb/transaction.ts b/packages/core/src/ckb/transaction.ts new file mode 100644 index 0000000..0d69d80 --- /dev/null +++ b/packages/core/src/ckb/transaction.ts @@ -0,0 +1,781 @@ +import { Bytes, BytesLike, bytesFrom } from "../bytes/index"; +import { mol } from "../molecule/index"; +import { Num, NumLike, numFromBytes, numToBytes } from "../num/index"; +import { apply } from "../utils/index"; +import { Script, ScriptLike, ScriptOpt } from "./script"; +import { hashCkb, Hasher, HasherCkb } from "../hasher"; + +/** + * @public + */ +export type OutPointLike = { + txHash: BytesLike; + index: NumLike; +}; +/** + * @public + */ +@mol.codec( + mol.struct({ + txHash: mol.Byte32, + index: mol.Uint32, + }), +) +export class OutPoint extends mol.Entity.Base() { + /** + * Creates an instance of OutPoint. + * + * @param txHash - The transaction hash. + * @param index - The index of the output in the transaction. + */ + + constructor( + public txHash: Bytes, + public index: Num, + ) { + super(); + } + + /** + * Creates an OutPoint instance from an OutPointLike object. + * + * @param outPoint - An OutPointLike object or an instance of OutPoint. + * @returns An OutPoint instance. + * + * @example + * ```typescript + * const outPoint = OutPoint.from({ txHash: "0x...", index: 0 }); + * ``` + */ + static from(outPoint: OutPointLike): OutPoint { + if (outPoint instanceof OutPoint) { + return outPoint; + } + return new OutPoint(bytesFrom(outPoint.txHash), outPoint.index); + } +} + +/** + * @public + */ +export type CellOutputLike = { + capacity: NumLike; + lock: ScriptLike; + type?: ScriptLike | null; +}; +/** + * @public + */ +@mol.codec( + mol.table({ + capacity: mol.Uint64, + lock: Script, + type: ScriptOpt, + }), +) +export class CellOutput extends mol.Entity.Base() { + /** + * Creates an instance of CellOutput. + * + * @param capacity - The capacity of the cell. + * @param lock - The lock script of the cell. + * @param type - The optional type script of the cell. + */ + + constructor( + public capacity: Num, + public lock: Script, + public type?: Script, + ) { + super(); + } + + get occupiedSize(): number { + return 8 + this.lock.occupiedSize + (this.type?.occupiedSize ?? 0); + } + + /** + * Creates a CellOutput instance from a CellOutputLike object. + * + * @param cellOutput - A CellOutputLike object or an instance of CellOutput. + * @returns A CellOutput instance. + * + * @example + * ```typescript + * const cellOutput = CellOutput.from({ + * capacity: 1000n, + * lock: { codeHash: "0x...", hashType: "type", args: "0x..." }, + * type: { codeHash: "0x...", hashType: "type", args: "0x..." } + * }); + * ``` + */ + static from(cellOutput: CellOutputLike): CellOutput { + if (cellOutput instanceof CellOutput) { + return cellOutput; + } + + return new CellOutput( + cellOutput.capacity, + Script.from(cellOutput.lock), + apply(Script.from, cellOutput.type), + ); + } +} +export const CellOutputVec = mol.vector(CellOutput); + +/** + * @public + */ +export type CellLike = { + outPoint: OutPointLike; + cellOutput: CellOutputLike; + outputData: BytesLike; +}; +/** + * @public + */ +export class Cell { + /** + * Creates an instance of Cell. + * + * @param outPoint - The output point of the cell. + * @param cellOutput - The cell output of the cell. + * @param outputData - The output data of the cell. + */ + + constructor( + public outPoint: OutPoint, + public cellOutput: CellOutput, + public outputData: BytesLike, + ) {} + + /** + * Creates a Cell instance from a CellLike object. + * + * @param cell - A CellLike object or an instance of Cell. + * @returns A Cell instance. + */ + + static from(cell: CellLike): Cell { + if (cell instanceof Cell) { + return cell; + } + + return new Cell( + OutPoint.from(cell.outPoint), + CellOutput.from(cell.cellOutput), + bytesFrom(cell.outputData), + ); + } + + /** + * Clone a Cell + * + * @returns A cloned Cell instance. + * + * @example + * ```typescript + * const cell1 = cell0.clone(); + * ``` + */ + clone(): Cell { + return new Cell( + this.outPoint.clone(), + this.cellOutput.clone(), + this.outputData, + ); + } +} + +// TODO: since + +/** + * @public + */ +export type CellInputLike = { + previousOutput: OutPointLike; + since?: NumLike | null; + cellOutput?: CellOutputLike | null; + outputData?: BytesLike | null; +}; +/** + * @public + */ +@mol.codec( + mol + .struct({ + since: mol.Uint64, + previousOutput: OutPoint, + }) + .mapIn((encodable: CellInputLike) => ({ + ...encodable, + since: encodable.since ?? 0, + })), +) +export class CellInput extends mol.Entity.Base() { + /** + * Creates an instance of CellInput. + * + * @param previousOutput - The previous outpoint of the cell. + * @param since - The since value of the cell input. + * @param cellOutput - The optional cell output associated with the cell input. + * @param outputData - The optional output data associated with the cell input. + */ + + constructor( + public previousOutput: OutPoint, + public since: Num, + public cellOutput?: CellOutput, + public outputData?: BytesLike, + ) { + super(); + } + + /** + * Creates a CellInput instance from a CellInputLike object. + * + * @param cellInput - A CellInputLike object or an instance of CellInput. + * @returns A CellInput instance. + * + * @example + * ```typescript + * const cellInput = CellInput.from({ + * previousOutput: { txHash: "0x...", index: 0 }, + * since: 0n + * }); + * ``` + */ + static from(cellInput: CellInputLike): CellInput { + if (cellInput instanceof CellInput) { + return cellInput; + } + + return new CellInput( + OutPoint.from(cellInput.previousOutput), + cellInput.since ?? 0, + apply(CellOutput.from, cellInput.cellOutput), + apply(bytesFrom, cellInput.outputData), + ); + } +} +export const CellInputVec = mol.vector(CellInput); + +/** + * @public + */ +export type CellDepLike = { + outPoint: OutPointLike; + depType: number; +}; +/** + * @public + */ +@mol.codec( + mol.struct({ + outPoint: OutPoint, + depType: mol.Uint8, + }), +) +export class CellDep extends mol.Entity.Base() { + /** + * Creates an instance of CellDep. + * + * @param outPoint - The outpoint of the cell dependency. + * @param depType - The dependency type. + */ + + constructor( + public outPoint: OutPoint, + public depType: number, + ) { + super(); + } + + /** + * Clone a CellDep. + * + * @returns A cloned CellDep instance. + * + * @example + * ```typescript + * const cellDep1 = cellDep0.clone(); + * ``` + */ + + clone(): CellDep { + return new CellDep(this.outPoint.clone(), this.depType); + } + + /** + * Creates a CellDep instance from a CellDepLike object. + * + * @param cellDep - A CellDepLike object or an instance of CellDep. + * @returns A CellDep instance. + * + * @example + * ```typescript + * const cellDep = CellDep.from({ + * outPoint: { txHash: "0x...", index: 0 }, + * depType: "depGroup" + * }); + * ``` + */ + + static from(cellDep: CellDepLike): CellDep { + if (cellDep instanceof CellDep) { + return cellDep; + } + + return new CellDep(OutPoint.from(cellDep.outPoint), cellDep.depType); + } +} +export const CellDepVec = mol.vector(CellDep); + +/** + * @public + */ +export type WitnessArgsLike = { + lock?: BytesLike | null; + inputType?: BytesLike | null; + outputType?: BytesLike | null; +}; +/** + * @public + */ +@mol.codec( + mol.table({ + lock: mol.BytesOpt, + inputType: mol.BytesOpt, + outputType: mol.BytesOpt, + }), +) +export class WitnessArgs extends mol.Entity.Base< + WitnessArgsLike, + WitnessArgs +>() { + /** + * Creates an instance of WitnessArgs. + * + * @param lock - The optional lock field of the witness. + * @param inputType - The optional input type field of the witness. + * @param outputType - The optional output type field of the witness. + */ + + constructor( + public lock?: Bytes, + public inputType?: Bytes, + public outputType?: Bytes, + ) { + super(); + } + + /** + * Creates a WitnessArgs instance from a WitnessArgsLike object. + * + * @param witnessArgs - A WitnessArgsLike object or an instance of WitnessArgs. + * @returns A WitnessArgs instance. + * + * @example + * ```typescript + * const witnessArgs = WitnessArgs.from({ + * lock: "0x...", + * inputType: "0x...", + * outputType: "0x..." + * }); + * ``` + */ + + static from(witnessArgs: WitnessArgsLike): WitnessArgs { + if (witnessArgs instanceof WitnessArgs) { + return witnessArgs; + } + + return new WitnessArgs( + apply(bytesFrom, witnessArgs.lock), + apply(bytesFrom, witnessArgs.inputType), + apply(bytesFrom, witnessArgs.outputType), + ); + } +} + +/** + * @public + */ +export function udtBalanceFrom(dataLike: BytesLike): Num { + const data = bytesFrom(dataLike).slice(0, 16); + return numFromBytes(data); +} + +export const RawTransaction = mol.table({ + version: mol.Uint32, + cellDeps: CellDepVec, + headerDeps: mol.Byte32Vec, + inputs: CellInputVec, + outputs: CellOutputVec, + outputsData: mol.BytesVec, +}); + +/** + * @public + */ +export type TransactionLike = { + version?: NumLike | null; + cellDeps?: CellDepLike[] | null; + headerDeps?: BytesLike[] | null; + inputs?: CellInputLike[] | null; + outputs?: + | (Omit & + Partial>)[] + | null; + outputsData?: BytesLike[] | null; + witnesses?: BytesLike[] | null; +}; +/** + * @public + */ +@mol.codec( + mol + .table({ + raw: RawTransaction, + witnesses: mol.BytesVec, + }) + .mapIn((txLike: TransactionLike) => { + const tx = Transaction.from(txLike); + return { + raw: tx, + witnesses: tx.witnesses, + }; + }) + .mapOut((tx) => Transaction.from({ ...tx.raw, witnesses: tx.witnesses })), +) +export class Transaction extends mol.Entity.Base< + TransactionLike, + Transaction +>() { + /** + * Creates an instance of Transaction. + * + * @param version - The version of the transaction. + * @param cellDeps - The cell dependencies of the transaction. + * @param headerDeps - The header dependencies of the transaction. + * @param inputs - The inputs of the transaction. + * @param outputs - The outputs of the transaction. + * @param outputsData - The data associated with the outputs. + * @param witnesses - The witnesses of the transaction. + */ + + constructor( + public version: Num, + public cellDeps: CellDep[], + public headerDeps: Bytes[], + public inputs: CellInput[], + public outputs: CellOutput[], + public outputsData: Bytes[], + public witnesses: Bytes[], + ) { + super(); + } + + /** + * Creates a default Transaction instance with empty fields. + * + * @returns A default Transaction instance. + * + * @example + * ```typescript + * const defaultTx = Transaction.default(); + * ``` + */ + static default(): Transaction { + return new Transaction(0, [], [], [], [], [], []); + } + + /** + * Copy every properties from another transaction. + * + * @example + * ```typescript + * this.copy(Transaction.default()); + * ``` + */ + copy(txLike: TransactionLike) { + const tx = Transaction.from(txLike); + this.version = tx.version; + this.cellDeps = tx.cellDeps; + this.headerDeps = tx.headerDeps; + this.inputs = tx.inputs; + this.outputs = tx.outputs; + this.outputsData = tx.outputsData; + this.witnesses = tx.witnesses; + } + + /** + * Creates a Transaction instance from a TransactionLike object. + * + * @param tx - A TransactionLike object or an instance of Transaction. + * @returns A Transaction instance. + * + * @example + * ```typescript + * const transaction = Transaction.from({ + * version: 0, + * cellDeps: [], + * headerDeps: [], + * inputs: [], + * outputs: [], + * outputsData: [], + * witnesses: [] + * }); + * ``` + */ + + static from(tx: TransactionLike): Transaction { + if (tx instanceof Transaction) { + return tx; + } + const outputs = + tx.outputs?.map((output, i) => { + const o = CellOutput.from({ + ...output, + capacity: output.capacity ?? 0, + }); + if (o.capacity === 0) { + o.capacity = + o.occupiedSize + + (apply(bytesFrom, tx.outputsData?.[i])?.length ?? 0); + } + return o; + }) ?? []; + const outputsData = outputs.map((_, i) => + bytesFrom(tx.outputsData?.[i] ?? new Uint8Array(0)), + ); + if (tx.outputsData != null && outputsData.length < tx.outputsData.length) { + outputsData.push( + ...tx.outputsData.slice(outputsData.length).map((d) => bytesFrom(d)), + ); + } + + return new Transaction( + tx.version ?? 0, + tx.cellDeps?.map((cellDep) => CellDep.from(cellDep)) ?? [], + tx.headerDeps?.map(bytesFrom) ?? [], + tx.inputs?.map((input) => CellInput.from(input)) ?? [], + outputs, + outputsData, + tx.witnesses?.map(bytesFrom) ?? [], + ); + } + + stringify(): string { + return JSON.stringify(this, (_, value) => { + if (typeof value === "bigint") { + return numToBytes(Number(value), 8); + } + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return value; + }); + } + + /** + * Converts the raw transaction data to bytes. + * + * @returns A Uint8Array containing the raw transaction bytes. + * + * @example + * ```typescript + * const rawTxBytes = transaction.rawToBytes(); + * ``` + */ + rawToBytes(): Bytes { + return RawTransaction.encode(this); + } + + /** + * Calculates the hash of the transaction without witnesses. This is the transaction hash in the usual sense. + * To calculate the hash of the whole transaction including the witnesses, use transaction.hashFull() instead. + * + * @returns The hash of the transaction. + * + * @example + * ```typescript + * const txHash = transaction.hash(); + * ``` + */ + hash(): Bytes { + return hashCkb(this.rawToBytes()); + } + + /** + * Calculates the hash of the transaction with witnesses. + * + * @returns The hash of the transaction with witnesses. + * + * @example + * ```typescript + * const txFullHash = transaction.hashFull(); + * ``` + */ + hashFull(): Bytes { + return hashCkb(this.toBytes()); + } + + /** + * Hashes a witness and updates the hasher. + * + * @param witness - The witness to hash. + * @param hasher - The hasher instance to update. + * + * @example + * ```typescript + * Transaction.hashWitnessToHasher("0x...", hasher); + * ``` + */ + static hashWitnessToHasher(witness: BytesLike, hasher: Hasher) { + const raw = bytesFrom(witness); + hasher.update(numToBytes(raw.length, 8)); + hasher.update(raw); + } + + /** + * Computes the signing hash information for a given script. + * + * @param scriptLike - The script associated with the transaction, represented as a ScriptLike object. + * @param client - The client for complete extra infos in the transaction. + * @returns A promise that resolves to an object containing the signing message and the witness position, + * or undefined if no matching input is found. + * + * @example + * ```typescript + * const signHashInfo = await tx.getSignHashInfo(scriptLike, client); + * if (signHashInfo) { + * console.log(signHashInfo.message); // Outputs the signing message + * console.log(signHashInfo.position); // Outputs the witness position + * } + * ``` + */ + getSignHashInfo( + scriptLike: ScriptLike, + hasher: Hasher = new HasherCkb(), + ): { message: Bytes; position: number } | undefined { + const script = Script.from(scriptLike); + let position = -1; + hasher.update(this.hash()); + + for (let i = 0; i < this.witnesses.length; i += 1) { + const input = this.inputs[i]; + if (input) { + // await input.completeExtraInfos(client); + + if (!input.cellOutput) { + throw new Error("Unable to complete input"); + } + + if (!script.eq(input.cellOutput.lock)) { + continue; + } + + if (position === -1) { + position = i; + } + } + + if (position === -1) { + return undefined; + } + + Transaction.hashWitnessToHasher(this.witnesses[i], hasher); + } + + if (position === -1) { + return undefined; + } + + return { + message: hasher.digest(), + position, + }; + } + /** + * Get witness at index as WitnessArgs + * + * @param index - The index of the witness. + * @returns The witness parsed as WitnessArgs. + * + * @example + * ```typescript + * const witnessArgs = await tx.getWitnessArgsAt(0); + * ``` + */ + getWitnessArgsAt(index: number): WitnessArgs | undefined { + const rawWitness = this.witnesses[index]; + return rawWitness.length > 0 + ? WitnessArgs.fromBytes(rawWitness) + : undefined; + } + + /** + * Set witness at index by WitnessArgs + * + * @param index - The index of the witness. + * @param witness - The WitnessArgs to set. + * + * @example + * ```typescript + * await tx.setWitnessArgsAt(0, witnessArgs); + * ``` + */ + setWitnessArgsAt(index: number, witness: WitnessArgs): void { + if (this.witnesses.length < index) { + this.witnesses.push( + ...Array.from( + new Array(index - this.witnesses.length), + (): Bytes => new Uint8Array(0), + ), + ); + } + + this.witnesses[index] = witness.toBytes(); + } + + findInputIndexByLock(scriptIdLike: ScriptLike): number | undefined { + const script = Script.from(scriptIdLike); + + for (let i = 0; i < this.inputs.length; i += 1) { + const input = this.inputs[i]; + if (!input.cellOutput) { + throw new Error("Unable to complete input"); + } + + if ( + script.codeHash === input.cellOutput.lock.codeHash && + script.hashType === input.cellOutput.lock.hashType + ) { + return i; + } + } + return undefined; + } + + /** + * Prepare dummy witness for sighash all method + * + * @param scriptLike - The script associated with the transaction, represented as a ScriptLike object. + * @param lockLen - The length of dummy lock bytes. + * @returns A promise that resolves to the prepared transaction + * + * @example + * ```typescript + * await tx.prepareSighashAllWitness(scriptLike, 85, client); + * ``` + */ + prepareSighashAllWitness(scriptLike: ScriptLike, lockLen: number): void { + const position = this.findInputIndexByLock(scriptLike); + if (position === undefined) { + return; + } + + const witness = this.getWitnessArgsAt(position) ?? WitnessArgs.from({}); + witness.lock = new Uint8Array(lockLen); + this.setWitnessArgsAt(position, witness); + } +} diff --git a/packages/core/src/hasher/advanced.ts b/packages/core/src/hasher/advanced.ts new file mode 100644 index 0000000..f48845b --- /dev/null +++ b/packages/core/src/hasher/advanced.ts @@ -0,0 +1,4 @@ +// ckb-default-hash string +export const CKB_BLAKE2B_PERSONAL = new Uint8Array([ + 99, 107, 98, 45, 100, 101, 102, 97, 117, 108, 116, 45, 104, 97, 115, 104, +]).buffer; diff --git a/packages/core/src/hasher/hasher.ts b/packages/core/src/hasher/hasher.ts new file mode 100644 index 0000000..e340988 --- /dev/null +++ b/packages/core/src/hasher/hasher.ts @@ -0,0 +1,37 @@ +import { BytesLike, Bytes } from "../bytes/index"; + +/** + * @public + */ +export interface Hasher { + /** + * Updates the hash with the given data. + * + * @param data - The data to update the hash with. + * @returns The current Hasher instance for chaining. + * + * @example + * ```typescript + * const hasher = new Hasher(); + * hasher.update("some data").update("more data"); + * const hash = hasher.digest(); + * ``` + */ + + update(data: BytesLike): Hasher; + + /** + * Finalizes the hash and returns the digest as a hexadecimal string. + * + * @returns The hexadecimal string representation of the hash. + * + * @example + * ```typescript + * const hasher = new Hasher(); + * hasher.update("some data"); + * const hash = hasher.digest(); // Outputs something like "0x..." + * ``` + */ + + digest(): Bytes; +} diff --git a/packages/core/src/hasher/hasherCkb.ts b/packages/core/src/hasher/hasherCkb.ts new file mode 100644 index 0000000..e959334 --- /dev/null +++ b/packages/core/src/hasher/hasherCkb.ts @@ -0,0 +1,105 @@ +import { Bytes, BytesLike, bytesFrom } from "../bytes/index"; +import { CellInput, CellInputLike } from "../ckb/index"; +import { NumLike, numToBytes } from "../num/index"; +import { CKB_BLAKE2B_PERSONAL } from "./advanced"; +import { Hasher } from "./hasher.js"; +import { Blake2b } from "@ckb-js-std/bindings"; + +/** + * @public + */ +export class HasherCkb implements Hasher { + private readonly hasher: Blake2b; + private outLength: number; + + /** + * Creates an instance of Hasher. + * + * @param outLength - The output length of the hash in bytes. Default is 32. + * @param personal - The personal string for the Blake2b algorithm. Default is CKB_BLAKE2B_PERSONAL. + */ + + constructor(outLength = 32, personal = CKB_BLAKE2B_PERSONAL) { + this.hasher = new Blake2b(personal); + this.outLength = outLength; + } + /** + * Updates the hash with the given data. + * + * @param data - The data to update the hash with. + * @returns The current Hasher instance for chaining. + * + * @example + * ```typescript + * const hasher = new Hasher(); + * hasher.update("some data").update("more data"); + * const hash = hasher.digest(); + * ``` + */ + + update(data: BytesLike): HasherCkb { + this.hasher.update(data); + return this; + } + + /** + * Finalizes the hash and returns the digest as a hexadecimal string. + * + * @returns The hexadecimal string representation of the hash. + * + * @example + * ```typescript + * const hasher = new Hasher(); + * hasher.update("some data"); + * const hash = hasher.digest(); // Outputs something like "0x..." + * ``` + */ + + digest(): Bytes { + let result = this.hasher.finalize(); + return new Uint8Array(result).slice(0, this.outLength); + } +} + +/** + * Computes the CKB hash of the given data using the Blake2b algorithm. + * @public + * + * @param data - The data to hash. + * @returns The hexadecimal string representation of the hash. + * + * @example + * ```typescript + * const hash = hashCkb("some data"); // Outputs something like "0x..." + * ``` + */ + +export function hashCkb(...data: BytesLike[]): Bytes { + const hasher = new HasherCkb(); + data.forEach((d) => hasher.update(d)); + return hasher.digest(); +} + +/** + * Computes the Type ID hash of the given data. + * @public + * + * @param cellInputLike - The first cell input of the transaction. + * @param outputIndex - The output index of the Type ID cell. + * @returns The hexadecimal string representation of the hash. + * + * @example + * ```typescript + * const hash = hashTypeId(cellInput, outputIndex); // Outputs something like "0x..." + * ``` + */ + +export function hashTypeId( + cellInputLike: CellInputLike, + outputIndex: NumLike, +): Bytes { + return hashCkb( + CellInput.from(cellInputLike).toBytes(), + numToBytes(outputIndex, 8), + ); +} diff --git a/packages/core/src/hasher/hasherKeecak256.ts b/packages/core/src/hasher/hasherKeecak256.ts new file mode 100644 index 0000000..64b538f --- /dev/null +++ b/packages/core/src/hasher/hasherKeecak256.ts @@ -0,0 +1,54 @@ +import { Bytes, BytesLike, bytesFrom } from "../bytes/index.js"; +import { Hasher } from "./hasher.js"; +import { Keccak256 } from "@ckb-js-std/bindings"; + +/** + * @public + */ +export class HasherKeecak256 implements Hasher { + private readonly hasher: Keccak256; + + /** + * Creates an instance of Hasher. + */ + + constructor() { + this.hasher = new Keccak256(); + } + + /** + * Updates the hash with the given data. + * + * @param data - The data to update the hash with. + * @returns The current Hasher instance for chaining. + * + * @example + * ```typescript + * const hasher = new Hasher(); + * hasher.update("some data").update("more data"); + * const hash = hasher.digest(); + * ``` + */ + + update(data: BytesLike): HasherKeecak256 { + this.hasher.update(data); + return this; + } + + /** + * Finalizes the hash and returns the digest as a hexadecimal string. + * + * @returns The hexadecimal string representation of the hash. + * + * @example + * ```typescript + * const hasher = new Hasher(); + * hasher.update("some data"); + * const hash = hasher.digest(); // Outputs something like "0x..." + * ``` + */ + + digest(): Bytes { + return new Uint8Array(this.hasher.finalize()); + } +} diff --git a/packages/core/src/hasher/index.ts b/packages/core/src/hasher/index.ts new file mode 100644 index 0000000..afe3efc --- /dev/null +++ b/packages/core/src/hasher/index.ts @@ -0,0 +1,3 @@ +export * from "./hasher.js"; +export * from "./hasherCkb.js"; +export * from "./hasherKeecak256.js"; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 72f29d1..caa92a7 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,2 +1,2 @@ -export * from "./barrel.js"; -export * as ckb from "./barrel.js"; +export * from "./barrel"; +export * as ckb from "./barrel"; diff --git a/packages/core/src/molecule/predefined.ts b/packages/core/src/molecule/predefined.ts index 07f0d2b..f165d31 100644 --- a/packages/core/src/molecule/predefined.ts +++ b/packages/core/src/molecule/predefined.ts @@ -59,32 +59,32 @@ export const BoolVec = vector(Bool); export const Byte4: Codec = Codec.from({ byteLength: 4, - encode: (value) => bytesFrom(value), - decode: (buffer) => bytesFrom(buffer), + encode: bytesFrom, + decode: bytesFrom, }); export const Byte4Opt = option(Byte4); export const Byte4Vec = vector(Byte4); export const Byte8: Codec = Codec.from({ byteLength: 8, - encode: (value) => bytesFrom(value), - decode: (buffer) => bytesFrom(buffer), + encode: bytesFrom, + decode: bytesFrom, }); export const Byte8Opt = option(Byte8); export const Byte8Vec = vector(Byte8); export const Byte16: Codec = Codec.from({ byteLength: 16, - encode: (value) => bytesFrom(value), - decode: (buffer) => bytesFrom(buffer), + encode: bytesFrom, + decode: bytesFrom, }); export const Byte16Opt = option(Byte16); export const Byte16Vec = vector(Byte16); export const Byte32: Codec = Codec.from({ byteLength: 32, - encode: (value) => bytesFrom(value), - decode: (buffer) => bytesFrom(buffer), + encode: bytesFrom, + decode: bytesFrom, }); export const Byte32Opt = option(Byte32); export const Byte32Vec = vector(Byte32); diff --git a/packages/core/src/utils/index.ts b/packages/core/src/utils/index.ts index 223618b..b51baf1 100644 --- a/packages/core/src/utils/index.ts +++ b/packages/core/src/utils/index.ts @@ -1,5 +1,3 @@ -import { NumLike } from "../num/index.js"; - /** * A type safe way to apply a transformer on a value if it's not empty. * @public diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json index bf5a36d..57315fd 100644 --- a/packages/core/tsconfig.json +++ b/packages/core/tsconfig.json @@ -1,4 +1,7 @@ { "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist" + }, "include": ["src/**/*"] } diff --git a/packages/examples/package.json b/packages/examples/package.json index e7f9f42..d257320 100644 --- a/packages/examples/package.json +++ b/packages/examples/package.json @@ -16,6 +16,8 @@ "scripts": { "start": "ckb-debugger --read-file dist/index.js --bin ../../build/ckb-js-vm -- -r", "build": "tsc --noEmit && esbuild --platform=neutral --minify --bundle --external:@ckb-js-std/bindings --target=es2022 src/index.ts --outfile=dist/index.js", + "compile": "ckb-debugger --read-file dist/index.js --bin ../../build/ckb-js-vm -- -c | awk -f ../../tools/compile.awk | xxd -r -p > dist/index.bc", + "start:bc": "ckb-debugger --read-file dist/index.bc --bin ../../build/ckb-js-vm -- -r", "clean": "rm -f dist/*" }, "devDependencies": { @@ -25,6 +27,7 @@ }, "dependencies": { "@ckb-js-std/bindings": "workspace:*", + "@ckb-js-std/core": "workspace:*", "@ckb-js-std/examples": "link:" } } diff --git a/packages/examples/src/index.ts b/packages/examples/src/index.ts index 9796119..d980bc1 100644 --- a/packages/examples/src/index.ts +++ b/packages/examples/src/index.ts @@ -1,10 +1,23 @@ import * as ckb from "@ckb-js-std/bindings"; +import { Script } from "@ckb-js-std/core"; + + +function report_cycles() { + let cycles = ckb.currentCycles(); + let num = (cycles/1024/1024).toFixed(2); + console.log(`current cycles = ${num} M`); +} function main() { + report_cycles(); let script = ckb.loadScript(); console.log(`script length is ${script.byteLength}`); - let cycles = ckb.currentCycles(); - console.log(`current cycles is ${cycles}`); + report_cycles(); + let script_obj = Script.decode(new Uint8Array(script)); + console.log("script code_hash = ", script_obj.codeHash); + console.log("script hash_type = ", script_obj.hashType); + console.log("script args = ", script_obj.args); + report_cycles(); } main(); diff --git a/packages/examples/tsconfig.json b/packages/examples/tsconfig.json index 34fc16c..cfa35ad 100644 --- a/packages/examples/tsconfig.json +++ b/packages/examples/tsconfig.json @@ -1,6 +1,9 @@ { - "extends": "../../tsconfig.base.json", - "include": [ - "src/**/*" - ] + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + }, + "include": [ + "src/**/*" + ] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d423e46..1c2d081 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,11 +26,23 @@ importers: packages/bindings: {} + packages/core: + dependencies: + '@ckb-js-std/bindings': + specifier: workspace:* + version: link:../bindings + '@ckb-js-std/core': + specifier: 'link:' + version: 'link:' + packages/examples: dependencies: '@ckb-js-std/bindings': specifier: workspace:* version: link:../bindings + '@ckb-js-std/core': + specifier: workspace:* + version: link:../core '@ckb-js-std/examples': specifier: 'link:' version: 'link:' diff --git a/tsconfig.base.json b/tsconfig.base.json index e73175b..2f9788e 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -4,7 +4,6 @@ "module": "ESNext", /* Use esbuild to bundle the code */ "moduleResolution": "Bundler", - "outDir": "./dist", "target": "ES2022", "incremental": true, "allowJs": true,