UNPKG

@ckb-ccc/core

Version:

Core of CCC - CKBer's Codebase

1,997 lines (1,816 loc) 50.8 kB
import { Bytes, BytesLike, bytesFrom } from "../bytes/index.js"; import type { ClientCollectableSearchKeyFilterLike } from "../client/clientTypes.advanced.js"; import { ClientBlockHeader, type CellDepInfoLike, type Client, type ClientBlockHeaderLike, } from "../client/index.js"; import { KnownScript } from "../client/knownScript.js"; import { Zero, fixedPointFrom, fixedPointToString, } from "../fixedPoint/index.js"; import { Hasher, HasherCkb, hashCkb } from "../hasher/index.js"; import { Hex, HexLike, hexFrom } from "../hex/index.js"; import { mol } from "../molecule/index.js"; import { Num, NumLike, numFrom, numFromBytes, numToBytes, numToHex, } from "../num/index.js"; import type { Signer } from "../signer/index.js"; import { apply, reduceAsync } from "../utils/index.js"; import { Script, ScriptLike, ScriptOpt } from "./script.js"; import { DEP_TYPE_TO_NUM, NUM_TO_DEP_TYPE } from "./transaction.advanced.js"; import type { LumosTransactionSkeletonType } from "./transactionLumos.js"; export const DepTypeCodec: mol.Codec<DepTypeLike, DepType> = mol.Codec.from({ byteLength: 1, encode: depTypeToBytes, decode: depTypeFromBytes, }); /** * @public */ export type DepTypeLike = string | number | bigint; /** * @public */ export type DepType = "depGroup" | "code"; /** * Converts a DepTypeLike value to a DepType. * @public * * @param val - The value to convert, which can be a string, number, or bigint. * @returns The corresponding DepType. * * @throws Will throw an error if the input value is not a valid dep type. * * @example * ```typescript * const depType = depTypeFrom(1); // Outputs "code" * const depType = depTypeFrom("depGroup"); // Outputs "depGroup" * ``` */ export function depTypeFrom(val: DepTypeLike): DepType { const depType = (() => { if (typeof val === "number") { return NUM_TO_DEP_TYPE[val]; } if (typeof val === "bigint") { return NUM_TO_DEP_TYPE[Number(val)]; } return val as DepType; })(); if (depType === undefined) { throw new Error(`Invalid dep type ${val}`); } return depType; } /** * Converts a DepTypeLike value to its corresponding byte representation. * @public * * @param depType - The dep type value to convert. * @returns A Uint8Array containing the byte representation of the dep type. * * @example * ```typescript * const depTypeBytes = depTypeToBytes("code"); // Outputs Uint8Array [1] * ``` */ export function depTypeToBytes(depType: DepTypeLike): Bytes { return bytesFrom([DEP_TYPE_TO_NUM[depTypeFrom(depType)]]); } /** * Converts a byte-like value to a DepType. * @public * * @param bytes - The byte-like value to convert. * @returns The corresponding DepType. * * @throws Will throw an error if the input bytes do not correspond to a valid dep type. * * @example * ```typescript * const depType = depTypeFromBytes(new Uint8Array([1])); // Outputs "code" * ``` */ export function depTypeFromBytes(bytes: BytesLike): DepType { return NUM_TO_DEP_TYPE[bytesFrom(bytes)[0]]; } /** * @public */ export type OutPointLike = { txHash: HexLike; index: NumLike; }; /** * @public */ @mol.codec( mol.struct({ txHash: mol.Byte32, index: mol.Uint32, }), ) export class OutPoint extends mol.Entity.Base<OutPointLike, OutPoint>() { /** * Creates an instance of OutPoint. * * @param txHash - The transaction hash. * @param index - The index of the output in the transaction. */ constructor( public txHash: Hex, 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(hexFrom(outPoint.txHash), numFrom(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<CellOutputLike, CellOutput>() { /** * 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( numFrom(cellOutput.capacity), Script.from(cellOutput.lock), apply(Script.from, cellOutput.type), ); } } export const CellOutputVec = mol.vector(CellOutput); /** * @public */ export type CellLike = ( | { outPoint: OutPointLike; } | { previousOutput: OutPointLike } ) & { cellOutput: CellOutputLike; outputData: HexLike; }; /** * @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: Hex, ) {} /** * 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("outPoint" in cell ? cell.outPoint : cell.previousOutput), CellOutput.from(cell.cellOutput), hexFrom(cell.outputData), ); } get capacityFree() { const occupiedSize = fixedPointFrom( this.cellOutput.occupiedSize + bytesFrom(this.outputData).length, ); return this.cellOutput.capacity - occupiedSize; } /** * Occupied bytes of a cell on chain * It's CellOutput.occupiedSize + bytesFrom(outputData).byteLength */ get occupiedSize() { return this.cellOutput.occupiedSize + bytesFrom(this.outputData).byteLength; } /** * Gets confirmed Nervos DAO profit of a Cell * It returns non-zero value only when the cell is in withdrawal phase 2 * See https://github.com/nervosnetwork/rfcs/blob/master/rfcs/0023-dao-deposit-withdraw/0023-dao-deposit-withdraw.md * * @param client - A client for searching DAO related headers * @returns Profit * * @example * ```typescript * const profit = await cell.getDaoProfit(client); * ``` */ async getDaoProfit(client: Client): Promise<Num> { if (!(await this.isNervosDao(client, "withdrew"))) { return Zero; } const { depositHeader, withdrawHeader } = await this.getNervosDaoInfo(client); if (!withdrawHeader || !depositHeader) { throw new Error( `Unable to get headers of a Nervos DAO cell ${this.outPoint.txHash}:${this.outPoint.index.toString()}`, ); } return calcDaoProfit(this.capacityFree, depositHeader, withdrawHeader); } async isNervosDao( client: Client, phase?: "deposited" | "withdrew", ): Promise<boolean> { const { type } = this.cellOutput; const daoType = await client.getKnownScript(KnownScript.NervosDao); if ( !type || type.codeHash !== daoType.codeHash || type.hashType !== daoType.hashType ) { // Non Nervos DAO cell return false; } const hasWithdrew = numFrom(this.outputData) !== Zero; return ( !phase || (phase === "deposited" && !hasWithdrew) || (phase === "withdrew" && hasWithdrew) ); } async getNervosDaoInfo(client: Client): Promise< // Non Nervos DAO cell | { depositHeader?: undefined; withdrawHeader?: undefined; } // Deposited Nervos DAO cell | { depositHeader: ClientBlockHeader; withdrawHeader?: undefined; } // Withdrew Nervos DAO cell | { depositHeader: ClientBlockHeader; withdrawHeader: ClientBlockHeader; } > { if (!(await this.isNervosDao(client))) { // Non Nervos DAO cell return {}; } if (numFrom(this.outputData) === Zero) { // Deposited Nervos DAO cell const depositRes = await client.getCellWithHeader(this.outPoint); if (!depositRes?.header) { throw new Error( `Unable to get header of a Nervos DAO deposited cell ${this.outPoint.txHash}:${this.outPoint.index.toString()}`, ); } return { depositHeader: depositRes.header, }; } // Withdrew Nervos DAO cell const [depositHeader, withdrawRes] = await Promise.all([ client.getHeaderByNumber(numFromBytes(this.outputData)), client.getCellWithHeader(this.outPoint), ]); if (!withdrawRes?.header || !depositHeader) { throw new Error( `Unable to get headers of a Nervos DAO withdrew cell ${this.outPoint.txHash}:${this.outPoint.index.toString()}`, ); } return { depositHeader, withdrawHeader: withdrawRes.header, }; } /** * 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, ); } } /** * @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 */ export type SinceLike = | { relative: "absolute" | "relative"; metric: "blockNumber" | "epoch" | "timestamp"; value: NumLike; } | NumLike; /** * @public */ @mol.codec( mol.Uint64.mapIn((encodable: SinceLike) => Since.from(encodable).toNum()), ) export class Since extends mol.Entity.Base<SinceLike, Since>() { /** * Creates an instance of Since. * * @param relative - Absolute or relative * @param metric - The metric of since * @param value - The value of since */ constructor( public relative: "absolute" | "relative", public metric: "blockNumber" | "epoch" | "timestamp", public value: Num, ) { super(); } /** * Clone a Since. * * @returns A cloned Since instance. * * @example * ```typescript * const since1 = since0.clone(); * ``` */ clone(): Since { return new Since(this.relative, this.metric, this.value); } /** * Creates a Since instance from a SinceLike object. * * @param since - A SinceLike object or an instance of Since. * @returns A Since instance. * * @example * ```typescript * const since = Since.from("0x1234567812345678"); * ``` */ static from(since: SinceLike): Since { if (since instanceof Since) { return since; } if (typeof since === "object" && "relative" in since) { return new Since(since.relative, since.metric, numFrom(since.value)); } return Since.fromNum(since); } /** * Converts the Since instance to num. * * @returns A num * * @example * ```typescript * const num = since.toNum(); * ``` */ toNum(): Num { return ( this.value | (this.relative === "absolute" ? Zero : numFrom("0x8000000000000000")) | { blockNumber: numFrom("0x0000000000000000"), epoch: numFrom("0x2000000000000000"), timestamp: numFrom("0x4000000000000000"), }[this.metric] ); } /** * Creates a Since instance from a num-like value. * * @param numLike - The num-like value to convert. * @returns A Since instance. * * @example * ```typescript * const since = Since.fromNum("0x0"); * ``` */ static fromNum(numLike: NumLike): Since { const num = numFrom(numLike); const relative = num >> numFrom(63) === Zero ? "absolute" : "relative"; const metric = (["blockNumber", "epoch", "timestamp"] as Since["metric"][])[ Number((num >> numFrom(61)) & numFrom(3)) ]; const value = num & numFrom("0x00ffffffffffffff"); return new Since(relative, metric, value); } } /** * @public */ export type CellInputLike = ( | { previousOutput: OutPointLike; } | { outPoint: OutPointLike } ) & { since?: SinceLike | NumLike | null; cellOutput?: CellOutputLike | null; outputData?: HexLike | null; }; /** * @public */ @mol.codec( mol .struct({ since: Since, previousOutput: OutPoint, }) .mapIn((encodable: CellInputLike) => CellInput.from(encodable)), ) export class CellInput extends mol.Entity.Base<CellInputLike, CellInput>() { /** * 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?: Hex, ) { 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( "previousOutput" in cellInput ? cellInput.previousOutput : cellInput.outPoint, ), Since.from(cellInput.since ?? 0).toNum(), apply(CellOutput.from, cellInput.cellOutput), apply(hexFrom, cellInput.outputData), ); } async getCell(client: Client): Promise<Cell> { await this.completeExtraInfos(client); if (!this.cellOutput || !this.outputData) { throw new Error("Unable to complete input"); } return Cell.from({ outPoint: this.previousOutput, cellOutput: this.cellOutput, outputData: this.outputData, }); } /** * Complete extra infos in the input. Including * - Previous cell output * - Previous cell data * The instance will be modified. * * @returns true if succeed. * @example * ```typescript * await cellInput.completeExtraInfos(client); * ``` */ async completeExtraInfos(client: Client): Promise<void> { if (this.cellOutput && this.outputData) { return; } const cell = await client.getCell(this.previousOutput); if (!cell) { return; } this.cellOutput = cell.cellOutput; this.outputData = cell.outputData; } /** * The extra capacity created when consume this input. * This is usually NervosDAO interest, see https://github.com/nervosnetwork/rfcs/blob/master/rfcs/0023-dao-deposit-withdraw/0023-dao-deposit-withdraw.md. * And it can also be miners' income. (But this is not implemented yet) */ async getExtraCapacity(client: Client): Promise<Num> { return (await this.getCell(client)).getDaoProfit(client); } clone(): CellInput { const cloned = super.clone(); cloned.cellOutput = this.cellOutput; cloned.outputData = this.outputData; return cloned; } } export const CellInputVec = mol.vector(CellInput); /** * @public */ export type CellDepLike = { outPoint: OutPointLike; depType: DepTypeLike; }; /** * @public */ @mol.codec( mol.struct({ outPoint: OutPoint, depType: DepTypeCodec, }), ) export class CellDep extends mol.Entity.Base<CellDepLike, CellDep>() { /** * Creates an instance of CellDep. * * @param outPoint - The outpoint of the cell dependency. * @param depType - The dependency type. */ constructor( public outPoint: OutPoint, public depType: DepType, ) { 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), depTypeFrom(cellDep.depType), ); } } export const CellDepVec = mol.vector(CellDep); /** * @public */ export type WitnessArgsLike = { lock?: HexLike | null; inputType?: HexLike | null; outputType?: HexLike | 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?: Hex, public inputType?: Hex, public outputType?: Hex, ) { 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(hexFrom, witnessArgs.lock), apply(hexFrom, witnessArgs.inputType), apply(hexFrom, witnessArgs.outputType), ); } } /** * Convert a bytes to a num. * * @public */ export function udtBalanceFrom(dataLike: BytesLike): Num { const data = bytesFrom(dataLike).slice(0, 16); return data.length === 0 ? Zero : 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?: HexLike[] | null; inputs?: CellInputLike[] | null; outputs?: | (Omit<CellOutputLike, "capacity"> & Partial<Pick<CellOutputLike, "capacity">>)[] | null; outputsData?: HexLike[] | null; witnesses?: HexLike[] | 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: Hex[], public inputs: CellInput[], public outputs: CellOutput[], public outputsData: Hex[], public witnesses: Hex[], ) { 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(0n, [], [], [], [], [], []); } /** * 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 === Zero) { o.capacity = fixedPointFrom( o.occupiedSize + (apply(bytesFrom, tx.outputsData?.[i])?.length ?? 0), ); } return o; }) ?? []; const outputsData = outputs.map((_, i) => hexFrom(tx.outputsData?.[i] ?? "0x"), ); if (tx.outputsData != null && outputsData.length < tx.outputsData.length) { outputsData.push( ...tx.outputsData.slice(outputsData.length).map((d) => hexFrom(d)), ); } return new Transaction( numFrom(tx.version ?? 0), tx.cellDeps?.map((cellDep) => CellDep.from(cellDep)) ?? [], tx.headerDeps?.map(hexFrom) ?? [], tx.inputs?.map((input) => CellInput.from(input)) ?? [], outputs, outputsData, tx.witnesses?.map(hexFrom) ?? [], ); } /** * Creates a Transaction instance from a Lumos skeleton. * * @param skeleton - The Lumos transaction skeleton. * @returns A Transaction instance. * * @throws Will throw an error if an input's outPoint is missing. * * @example * ```typescript * const transaction = Transaction.fromLumosSkeleton(skeleton); * ``` */ static fromLumosSkeleton( skeleton: LumosTransactionSkeletonType, ): Transaction { return Transaction.from({ version: 0n, cellDeps: skeleton.cellDeps.toArray(), headerDeps: skeleton.headerDeps.toArray(), inputs: skeleton.inputs.toArray().map((input, i) => { if (!input.outPoint) { throw new Error("outPoint is required in input"); } return CellInput.from({ previousOutput: input.outPoint, since: skeleton.inputSinces.get(i, "0x0"), cellOutput: input.cellOutput, outputData: input.data, }); }), outputs: skeleton.outputs.toArray().map((output) => output.cellOutput), outputsData: skeleton.outputs.toArray().map((output) => output.data), witnesses: skeleton.witnesses.toArray(), }); } /** * @deprecated * Use ccc.stringify instead. * stringify the tx to JSON string. */ stringify(): string { return JSON.stringify(this, (_, value) => { if (typeof value === "bigint") { return numToHex(value); } // 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(): Hex { 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(): Hex { 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: HexLike, hasher: Hasher) { const raw = bytesFrom(hexFrom(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 * } * ``` */ async getSignHashInfo( scriptLike: ScriptLike, client: Client, hasher: Hasher = new HasherCkb(), ): Promise<{ message: Hex; 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) { const { cellOutput } = await input.getCell(client); if (!script.eq(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, }; } /** * Find the first occurrence of a input with the specified lock id * * @param scriptIdLike - The script associated with the transaction, represented as a ScriptLike object without args. * @param client - The client for complete extra infos in the transaction. * @returns A promise that resolves to the found index * * @example * ```typescript * const index = await tx.findInputIndexByLockId(scriptIdLike, client); * ``` */ async findInputIndexByLockId( scriptIdLike: Pick<ScriptLike, "codeHash" | "hashType">, client: Client, ): Promise<number | undefined> { const script = Script.from({ ...scriptIdLike, args: "0x" }); for (let i = 0; i < this.inputs.length; i += 1) { const { cellOutput } = await this.inputs[i].getCell(client); if ( script.codeHash === cellOutput.lock.codeHash && script.hashType === cellOutput.lock.hashType ) { return i; } } } /** * Find the first occurrence of a input with the specified lock * * @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 the prepared transaction * * @example * ```typescript * const index = await tx.findInputIndexByLock(scriptLike, client); * ``` */ async findInputIndexByLock( scriptLike: ScriptLike, client: Client, ): Promise<number | undefined> { const script = Script.from(scriptLike); for (let i = 0; i < this.inputs.length; i += 1) { const { cellOutput } = await this.inputs[i].getCell(client); if (script.eq(cellOutput.lock)) { return i; } } } /** * Find the last occurrence of a input with the specified lock * * @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 the prepared transaction * * @example * ```typescript * const index = await tx.findLastInputIndexByLock(scriptLike, client); * ``` */ async findLastInputIndexByLock( scriptLike: ScriptLike, client: Client, ): Promise<number | undefined> { const script = Script.from(scriptLike); for (let i = this.inputs.length - 1; i >= 0; i -= 1) { const { cellOutput } = await this.inputs[i].getCell(client); if (script.eq(cellOutput.lock)) { return i; } } } /** * Add cell deps if they are not existed * * @param cellDepLikes - The cell deps to add * * @example * ```typescript * tx.addCellDeps(cellDep); * ``` */ addCellDeps(...cellDepLikes: (CellDepLike | CellDepLike[])[]): void { cellDepLikes.flat().forEach((cellDepLike) => { const cellDep = CellDep.from(cellDepLike); if (this.cellDeps.some((c) => c.eq(cellDep))) { return; } this.cellDeps.push(cellDep); }); } /** * Add cell deps at the start if they are not existed * * @param cellDepLikes - The cell deps to add * * @example * ```typescript * tx.addCellDepsAtBegin(cellDep); * ``` */ addCellDepsAtStart(...cellDepLikes: (CellDepLike | CellDepLike[])[]): void { cellDepLikes.flat().forEach((cellDepLike) => { const cellDep = CellDep.from(cellDepLike); if (this.cellDeps.some((c) => c.eq(cellDep))) { return; } this.cellDeps.unshift(cellDep); }); } /** * Add cell dep from infos if they are not existed * * @param client - A client for searching cell deps * @param cellDepInfoLikes - The cell dep infos to add * * @example * ```typescript * tx.addCellDepInfos(client, cellDepInfos); * ``` */ async addCellDepInfos( client: Client, ...cellDepInfoLikes: (CellDepInfoLike | CellDepInfoLike[])[] ): Promise<void> { this.addCellDeps(await client.getCellDeps(...cellDepInfoLikes)); } /** * Add cell deps from known script * * @param client - The client for searching known script and cell deps * @param scripts - The known scripts to add * * @example * ```typescript * tx.addCellDepsOfKnownScripts(client, KnownScript.OmniLock); * ``` */ async addCellDepsOfKnownScripts( client: Client, ...scripts: (KnownScript | KnownScript[])[] ): Promise<void> { await Promise.all( scripts .flat() .map(async (script) => this.addCellDepInfos( client, (await client.getKnownScript(script)).cellDeps, ), ), ); } /** * Set output data at index. * * @param index - The index of the output data. * @param witness - The data to set. * * @example * ```typescript * await tx.setOutputDataAt(0, "0x00"); * ``` */ setOutputDataAt(index: number, witness: HexLike): void { if (this.outputsData.length < index) { this.outputsData.push( ...Array.from( new Array(index - this.outputsData.length), (): Hex => "0x", ), ); } this.outputsData[index] = hexFrom(witness); } /** * get input * * @param index - The cell input index * * @example * ```typescript * await tx.getInput(0); * ``` */ getInput(index: NumLike): CellInput | undefined { return this.inputs[Number(numFrom(index))]; } /** * add input * * @param inputLike - The cell input. * * @example * ```typescript * await tx.addInput({ }); * ``` */ addInput(inputLike: CellInputLike): number { if (this.witnesses.length > this.inputs.length) { this.witnesses.splice(this.inputs.length, 0, "0x"); } return this.inputs.push(CellInput.from(inputLike)); } /** * get output * * @param index - The cell output index * * @example * ```typescript * await tx.getOutput(0); * ``` */ getOutput(index: NumLike): | { cellOutput: CellOutput; outputData: Hex; } | undefined { const i = Number(numFrom(index)); if (i >= this.outputs.length) { return; } return { cellOutput: this.outputs[i], outputData: this.outputsData[i] ?? "0x", }; } /** * Add output * * @param outputLike - The cell output to add * @param outputData - optional output data * * @example * ```typescript * await tx.addOutput(cellOutput, "0xabcd"); * ``` */ addOutput( outputLike: Omit<CellOutputLike, "capacity"> & Partial<Pick<CellOutputLike, "capacity">>, outputData: HexLike = "0x", ): number { const output = CellOutput.from({ ...outputLike, capacity: outputLike.capacity ?? 0, }); if (output.capacity === Zero) { output.capacity = fixedPointFrom( output.occupiedSize + bytesFrom(outputData).length, ); } const len = this.outputs.push(output); this.setOutputDataAt(len - 1, outputData); return len; } /** * 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 ?? "0x") !== "0x" ? 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 { this.setWitnessAt(index, witness.toBytes()); } /** * Set witness at index * * @param index - The index of the witness. * @param witness - The witness to set. * * @example * ```typescript * await tx.setWitnessAt(0, witness); * ``` */ setWitnessAt(index: number, witness: HexLike): void { if (this.witnesses.length < index) { this.witnesses.push( ...Array.from( new Array(index - this.witnesses.length), (): Hex => "0x", ), ); } this.witnesses[index] = hexFrom(witness); } /** * 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. * @param client - The client for complete extra infos in the transaction. * @returns A promise that resolves to the prepared transaction * * @example * ```typescript * await tx.prepareSighashAllWitness(scriptLike, 85, client); * ``` */ async prepareSighashAllWitness( scriptLike: ScriptLike, lockLen: number, client: Client, ): Promise<void> { const position = await this.findInputIndexByLock(scriptLike, client); if (position === undefined) { return; } const witness = this.getWitnessArgsAt(position) ?? WitnessArgs.from({}); witness.lock = hexFrom(Array.from(new Array(lockLen), () => 0)); this.setWitnessArgsAt(position, witness); } async getInputsCapacityExtra(client: Client): Promise<Num> { return reduceAsync( this.inputs, async (acc, input) => acc + (await input.getExtraCapacity(client)), numFrom(0), ); } // This also includes extra amount async getInputsCapacity(client: Client): Promise<Num> { return ( (await reduceAsync( this.inputs, async (acc, input) => { const { cellOutput: { capacity }, } = await input.getCell(client); return acc + capacity; }, numFrom(0), )) + (await this.getInputsCapacityExtra(client)) ); } getOutputsCapacity(): Num { return this.outputs.reduce( (acc, { capacity }) => acc + capacity, numFrom(0), ); } async getInputsUdtBalance(client: Client, type: ScriptLike): Promise<Num> { return reduceAsync( this.inputs, async (acc, input) => { const { cellOutput, outputData } = await input.getCell(client); if (!cellOutput.type?.eq(type)) { return; } return acc + udtBalanceFrom(outputData); }, numFrom(0), ); } getOutputsUdtBalance(type: ScriptLike): Num { return this.outputs.reduce((acc, output, i) => { if (!output.type?.eq(type)) { return acc; } return acc + udtBalanceFrom(this.outputsData[i]); }, numFrom(0)); } async completeInputs<T>( from: Signer, filter: ClientCollectableSearchKeyFilterLike, accumulator: ( acc: T, v: Cell, i: number, array: Cell[], ) => Promise<T | undefined> | T | undefined, init: T, ): Promise<{ addedCount: number; accumulated?: T; }> { const collectedCells = []; let acc: T = init; let fulfilled = false; for await (const cell of from.findCells(filter, true)) { if ( this.inputs.some(({ previousOutput }) => previousOutput.eq(cell.outPoint), ) ) { continue; } const i = collectedCells.push(cell); const next = await Promise.resolve( accumulator(acc, cell, i - 1, collectedCells), ); if (next === undefined) { fulfilled = true; break; } acc = next; } collectedCells.forEach((cell) => this.addInput(cell)); if (fulfilled) { return { addedCount: collectedCells.length, }; } return { addedCount: collectedCells.length, accumulated: acc, }; } async completeInputsByCapacity( from: Signer, capacityTweak?: NumLike, filter?: ClientCollectableSearchKeyFilterLike, ): Promise<number> { const expectedCapacity = this.getOutputsCapacity() + numFrom(capacityTweak ?? 0); const inputsCapacity = await this.getInputsCapacity(from.client); if (inputsCapacity >= expectedCapacity) { return 0; } const { addedCount, accumulated } = await this.completeInputs( from, filter ?? { scriptLenRange: [0, 1], outputDataLenRange: [0, 1], }, (acc, { cellOutput: { capacity } }) => { const sum = acc + capacity; return sum >= expectedCapacity ? undefined : sum; }, inputsCapacity, ); if (accumulated === undefined) { return addedCount; } throw new Error( `Insufficient CKB, need ${fixedPointToString(expectedCapacity - accumulated)} extra CKB`, ); } async completeInputsAll( from: Signer, filter?: ClientCollectableSearchKeyFilterLike, ): Promise<number> { const { addedCount } = await this.completeInputs( from, filter ?? { scriptLenRange: [0, 1], outputDataLenRange: [0, 1], }, (acc, { cellOutput: { capacity } }) => acc + capacity, Zero, ); return addedCount; } /** * Complete inputs by UDT balance * * This method succeeds only if enough balance is collected. * * It will try to collect at least two inputs, even when the first input already contains enough balance, to avoid extra occupation fees introduced by the change cell. An edge case: If the first cell has the same amount as the output, a new cell is not needed. * @param from - The signer to complete the inputs. * @param type - The type script of the UDT. * @param balanceTweak - The tweak of the balance. * @returns A promise that resolves to the number of inputs added. */ async completeInputsByUdt( from: Signer, type: ScriptLike, balanceTweak?: NumLike, ): Promise<number> { const expectedBalance = this.getOutputsUdtBalance(type) + numFrom(balanceTweak ?? 0); if (expectedBalance === numFrom(0)) { return 0; } const [inputsBalance, inputsCount] = await reduceAsync( this.inputs, async ([balanceAcc, countAcc], input) => { const { cellOutput, outputData } = await input.getCell(from.client); if (!cellOutput.type?.eq(type)) { return; } return [balanceAcc + udtBalanceFrom(outputData), countAcc + 1]; }, [numFrom(0), 0], ); if ( inputsBalance === expectedBalance || (inputsBalance >= expectedBalance && inputsCount >= 2) ) { return 0; } const { addedCount, accumulated } = await this.completeInputs( from, { script: type, outputDataLenRange: [16, numFrom("0xffffffff")], }, (acc, { outputData }, _i, collected) => { const balance = udtBalanceFrom(outputData); const sum = acc + balance; return sum === expectedBalance || (sum >= expectedBalance && inputsCount + collected.length >= 2) ? undefined : sum; }, inputsBalance, ); if (accumulated === undefined || accumulated >= expectedBalance) { return addedCount; } throw new Error( `Insufficient coin, need ${expectedBalance - accumulated} extra coin`, ); } async completeInputsAddOne( from: Signer, filter?: ClientCollectableSearchKeyFilterLike, ): Promise<number> { const { addedCount, accumulated } = await this.completeInputs( from, filter ?? { scriptLenRange: [0, 1], outputDataLenRange: [0, 1], }, () => undefined, true, ); if (accumulated === undefined) { return addedCount; } throw new Error(`Insufficient CKB, need at least one new cell`); } async completeInputsAtLeastOne( from: Signer, filter?: ClientCollectableSearchKeyFilterLike, ): Promise<number> { if (this.inputs.length > 0) { return 0; } return this.completeInputsAddOne(from, filter); } async getFee(client: Client): Promise<Num> { return (await this.getInputsCapacity(client)) - this.getOutputsCapacity(); } async getFeeRate(client: Client): Promise<Num> { return ( ((await this.getFee(client)) * numFrom(1000)) / numFrom(this.toBytes().length + 4) ); } estimateFee(feeRate: NumLike): Num { const txSize = this.toBytes().length + 4; // + 999 then / 1000 to ceil the calculated fee return (numFrom(txSize) * numFrom(feeRate) + numFrom(999)) / numFrom(1000); } async completeFee( from: Signer, change: (tx: Transaction, capacity: Num) => Promise<NumLike> | NumLike, expectedFeeRate?: NumLike, filter?: ClientCollectableSearchKeyFilterLike, options?: { feeRateBlockRange?: NumLike; maxFeeRate?: NumLike }, ): Promise<[number, boolean]> { const feeRate = expectedFeeRate ?? (await from.client.getFeeRate(options?.feeRateBlockRange, options)); // Complete all inputs extra infos for cache await this.getInputsCapacity(from.client); let leastFee = Zero; let leastExtraCapacity = Zero; while (true) { const tx = this.clone(); const collected = await (async () => { try { return await tx.completeInputsByCapacity( from, leastFee + leastExtraCapacity, filter, ); } catch (err) { if (leastExtraCapacity !== Zero) { throw new Error("Not enough capacity for the change cell"); } throw err; } })(); await from.prepareTransaction(tx); if (leastFee === Zero) { // The initial fee is calculated based on prepared transaction leastFee = tx.estimateFee(feeRate); } const fee = await tx.getFee(from.client); // The extra capacity paid the fee without a change if (fee === leastFee) { this.copy(tx); return [collected, false]; } const needed = numFrom(await Promise.resolve(change(tx, fee - leastFee))); // No enough extra capacity to create new cells for change if (needed > Zero) { leastExtraCapacity = needed; continue; } if ((await tx.getFee(from.client)) !== leastFee) { throw new Error( "The change function doesn't use all available capacity", ); } // New change cells created, update the fee await from.prepareTransaction(tx); const changedFee = tx.estimateFee(feeRate); if (leastFee > changedFee) { throw new Error("The change function removed existed transaction data"); } // The fee has been paid if (leastFee === changedFee) { this.copy(tx); return [collected, true]; } // The fee after changing is more than the original fee leastFee = changedFee; } } completeFeeChangeToLock( from: Signer, change: ScriptLike, feeRate?: NumLike, filter?: ClientCollectableSearchKeyFilterLike, ): Promise<[number, boolean]> { const script = Script.from(change); return this.completeFee( from, (tx, capacity) => { const changeCell = CellOutput.from({ capacity: 0, lock: script }); const occupiedCapacity = fixedPointFrom(changeCell.occupiedSize); if (capacity < occupiedCapacity) { return occupiedCapacity; } changeCell.capacity = capacity; tx.addOutput(changeCell); return 0; }, feeRate, filter, ); } async completeFeeBy( from: Signer, feeRate?: NumLike, filter?: ClientCollectableSearchKeyFilterLike, ): Promise<[number, boolean]> { const { script } = await from.getRecommendedAddressObj(); return this.completeFeeChangeToLock(from, script, feeRate, filter); } completeFeeChangeToOutput( from: Signer, index: NumLike, feeRate?: NumLike, filter?: ClientCollectableSearchKeyFilterLike, ): Promise<[number, boolean]> { const change = Number(numFrom(index)); if (!this.outputs[change]) { throw new Error("Non-existed output to change"); } return this.completeFee( from, (tx, capacity) => { tx.outputs[change].capacity += capacity; return 0; }, feeRate, filter, ); } } /** * Calculate Nervos DAO profit between two blocks */ export function calcDaoProfit( profitableCapacity: NumLike, depositHeaderLike: ClientBlockHeaderLike, withdrawHeaderLike: ClientBlockHeaderLike, ): Num { const depositHeader = ClientBlockHeader.from(depositHeaderLike); const withdrawHeader = ClientBlockHeader.from(withdrawHeaderLike); const profitableSize = numFrom(profitableCapacity); return ( (profitableSize * withdrawHeader.dao.ar) / depositHeader.dao.ar - profitableSize ); } /** * Calculate claimable epoch for Nervos DAO withdrawal * See https://github.com/nervosnetwork/rfcs/blob/master/rfcs/0023-dao-deposit-withdraw/0023-dao-deposit-withdraw.md */ export function calcDaoClaimEpoch( depositHeader: Clie