UNPKG

@ckb-ccc/core

Version:

Core of CCC - CKBer's Codebase

1,457 lines (1,456 loc) 49.3 kB
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var OutPoint_1, CellOutput_1, Since_1, CellInput_1, CellDep_1, WitnessArgs_1, Transaction_1; import { bytesFrom } from "../bytes/index.js"; import { ClientBlockHeader, } from "../client/index.js"; import { KnownScript } from "../client/knownScript.js"; import { Zero, fixedPointFrom, fixedPointToString, } from "../fixedPoint/index.js"; import { HasherCkb, hashCkb } from "../hasher/index.js"; import { hexFrom } from "../hex/index.js"; import { mol } from "../molecule/index.js"; import { numFrom, numFromBytes, numToBytes, numToHex, } from "../num/index.js"; import { apply, reduceAsync } from "../utils/index.js"; import { Script, ScriptOpt } from "./script.js"; import { DEP_TYPE_TO_NUM, NUM_TO_DEP_TYPE } from "./transaction.advanced.js"; export const DepTypeCodec = mol.Codec.from({ byteLength: 1, encode: depTypeToBytes, decode: depTypeFromBytes, }); /** * 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) { 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; })(); 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) { 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) { return NUM_TO_DEP_TYPE[bytesFrom(bytes)[0]]; } /** * @public */ let OutPoint = OutPoint_1 = 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(txHash, index) { super(); this.txHash = txHash; this.index = index; } /** * 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) { if (outPoint instanceof OutPoint_1) { return outPoint; } return new OutPoint_1(hexFrom(outPoint.txHash), numFrom(outPoint.index)); } }; OutPoint = OutPoint_1 = __decorate([ mol.codec(mol.struct({ txHash: mol.Byte32, index: mol.Uint32, })) ], OutPoint); export { OutPoint }; /** * @public */ let CellOutput = CellOutput_1 = 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(capacity, lock, type) { super(); this.capacity = capacity; this.lock = lock; this.type = type; } get occupiedSize() { 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) { if (cellOutput instanceof CellOutput_1) { return cellOutput; } return new CellOutput_1(numFrom(cellOutput.capacity), Script.from(cellOutput.lock), apply(Script.from, cellOutput.type)); } }; CellOutput = CellOutput_1 = __decorate([ mol.codec(mol.table({ capacity: mol.Uint64, lock: Script, type: ScriptOpt, })) ], CellOutput); export { CellOutput }; export const CellOutputVec = mol.vector(CellOutput); /** * @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(outPoint, cellOutput, outputData) { this.outPoint = outPoint; this.cellOutput = cellOutput; this.outputData = outputData; } /** * 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) { 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) { 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, phase) { 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) { 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() { return new Cell(this.outPoint.clone(), this.cellOutput.clone(), this.outputData); } } /** * @public */ export function epochFrom(epochLike) { return [numFrom(epochLike[0]), numFrom(epochLike[1]), numFrom(epochLike[2])]; } /** * @public */ export function epochFromHex(hex) { const num = numFrom(hexFrom(hex)); return [ num & numFrom("0xffffff"), (num >> numFrom(24)) & numFrom("0xffff"), (num >> numFrom(40)) & numFrom("0xffff"), ]; } /** * @public */ export function epochToHex(epochLike) { const epoch = epochFrom(epochLike); return numToHex(numFrom(epoch[0]) + (numFrom(epoch[1]) << numFrom(24)) + (numFrom(epoch[2]) << numFrom(40))); } /** * @public */ let Since = Since_1 = class Since extends mol.Entity.Base() { /** * Creates an instance of Since. * * @param relative - Absolute or relative * @param metric - The metric of since * @param value - The value of since */ constructor(relative, metric, value) { super(); this.relative = relative; this.metric = metric; this.value = value; } /** * Clone a Since. * * @returns A cloned Since instance. * * @example * ```typescript * const since1 = since0.clone(); * ``` */ clone() { return new Since_1(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) { if (since instanceof Since_1) { return since; } if (typeof since === "object" && "relative" in since) { return new Since_1(since.relative, since.metric, numFrom(since.value)); } return Since_1.fromNum(since); } /** * Converts the Since instance to num. * * @returns A num * * @example * ```typescript * const num = since.toNum(); * ``` */ toNum() { 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) { const num = numFrom(numLike); const relative = num >> numFrom(63) === Zero ? "absolute" : "relative"; const metric = ["blockNumber", "epoch", "timestamp"][Number((num >> numFrom(61)) & numFrom(3))]; const value = num & numFrom("0x00ffffffffffffff"); return new Since_1(relative, metric, value); } }; Since = Since_1 = __decorate([ mol.codec(mol.Uint64.mapIn((encodable) => Since.from(encodable).toNum())) ], Since); export { Since }; /** * @public */ let CellInput = CellInput_1 = 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(previousOutput, since, cellOutput, outputData) { super(); this.previousOutput = previousOutput; this.since = since; this.cellOutput = cellOutput; this.outputData = outputData; } /** * 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) { if (cellInput instanceof CellInput_1) { return cellInput; } return new CellInput_1(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) { 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) { 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) { return (await this.getCell(client)).getDaoProfit(client); } clone() { const cloned = super.clone(); cloned.cellOutput = this.cellOutput; cloned.outputData = this.outputData; return cloned; } }; CellInput = CellInput_1 = __decorate([ mol.codec(mol .struct({ since: Since, previousOutput: OutPoint, }) .mapIn((encodable) => CellInput.from(encodable))) ], CellInput); export { CellInput }; export const CellInputVec = mol.vector(CellInput); /** * @public */ let CellDep = CellDep_1 = 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(outPoint, depType) { super(); this.outPoint = outPoint; this.depType = depType; } /** * Clone a CellDep. * * @returns A cloned CellDep instance. * * @example * ```typescript * const cellDep1 = cellDep0.clone(); * ``` */ clone() { return new CellDep_1(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) { if (cellDep instanceof CellDep_1) { return cellDep; } return new CellDep_1(OutPoint.from(cellDep.outPoint), depTypeFrom(cellDep.depType)); } }; CellDep = CellDep_1 = __decorate([ mol.codec(mol.struct({ outPoint: OutPoint, depType: DepTypeCodec, })) ], CellDep); export { CellDep }; export const CellDepVec = mol.vector(CellDep); /** * @public */ let WitnessArgs = WitnessArgs_1 = class WitnessArgs extends mol.Entity.Base() { /** * 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(lock, inputType, outputType) { super(); this.lock = lock; this.inputType = inputType; this.outputType = outputType; } /** * 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) { if (witnessArgs instanceof WitnessArgs_1) { return witnessArgs; } return new WitnessArgs_1(apply(hexFrom, witnessArgs.lock), apply(hexFrom, witnessArgs.inputType), apply(hexFrom, witnessArgs.outputType)); } }; WitnessArgs = WitnessArgs_1 = __decorate([ mol.codec(mol.table({ lock: mol.BytesOpt, inputType: mol.BytesOpt, outputType: mol.BytesOpt, })) ], WitnessArgs); export { WitnessArgs }; /** * Convert a bytes to a num. * * @public */ export function udtBalanceFrom(dataLike) { 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 */ let Transaction = Transaction_1 = class Transaction extends mol.Entity.Base() { /** * 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(version, cellDeps, headerDeps, inputs, outputs, outputsData, witnesses) { super(); this.version = version; this.cellDeps = cellDeps; this.headerDeps = headerDeps; this.inputs = inputs; this.outputs = outputs; this.outputsData = outputsData; this.witnesses = witnesses; } /** * Creates a default Transaction instance with empty fields. * * @returns A default Transaction instance. * * @example * ```typescript * const defaultTx = Transaction.default(); * ``` */ static default() { return new Transaction_1(0n, [], [], [], [], [], []); } /** * Copy every properties from another transaction. * * @example * ```typescript * this.copy(Transaction.default()); * ``` */ copy(txLike) { const tx = Transaction_1.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) { if (tx instanceof Transaction_1) { 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_1(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) { return Transaction_1.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() { 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() { 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() { 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() { 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, 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, client, hasher = new HasherCkb()) { 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_1.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, client) { 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, client) { 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, client) { 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) { 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) { 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, ...cellDepInfoLikes) { 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, ...scripts) { 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, witness) { if (this.outputsData.length < index) { this.outputsData.push(...Array.from(new Array(index - this.outputsData.length), () => "0x")); } this.outputsData[index] = hexFrom(witness); } /** * get input * * @param index - The cell input index * * @example * ```typescript * await tx.getInput(0); * ``` */ getInput(index) { return this.inputs[Number(numFrom(index))]; } /** * add input * * @param inputLike - The cell input. * * @example * ```typescript * await tx.addInput({ }); * ``` */ addInput(inputLike) { 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) { 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, outputData = "0x") { 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) { 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, witness) { 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, witness) { if (this.witnesses.length < index) { this.witnesses.push(...Array.from(new Array(index - this.witnesses.length), () => "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, lockLen, client) { 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) { return reduceAsync(this.inputs, async (acc, input) => acc + (await input.getExtraCapacity(client)), numFrom(0)); } // This also includes extra amount async getInputsCapacity(client) { 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() { return this.outputs.reduce((acc, { capacity }) => acc + capacity, numFrom(0)); } async getInputsUdtBalance(client, type) { 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) { return this.outputs.reduce((acc, output, i) => { if (!output.type?.eq(type)) { return acc; } return acc + udtBalanceFrom(this.outputsData[i]); }, numFrom(0)); } async completeInputs(from, filter, accumulator, init) { const collectedCells = []; let acc = 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, capacityTweak, filter) { 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, filter) { 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, type, balanceTweak) { 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, filter) { 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, filter) { if (this.inputs.length > 0) { return 0; } return this.completeInputsAddOne(from, filter); } async getFee(client) { return (await this.getInputsCapacity(client)) - this.getOutputsCapacity(); } async getFeeRate(client) { return (((await this.getFee(client)) * numFrom(1000)) / numFrom(this.toBytes().length + 4)); } estimateFee(feeRate) { 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, change, expectedFeeRate, filter, options) { 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, change, feeRate, filter) { 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, feeRate, filter) { const { script } = await from.getRecommendedAddressObj(); return this.completeFeeChangeToLock(from, script, feeRate, filter); } completeFeeChangeToOutput(from, index, feeRate, filter) { 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); } }; Transaction = Transaction_1 = __decorate([ mol.codec(mol .table({ raw: RawTransaction, witnesses: mol.BytesVec, }) .mapIn((txLike) => { const tx = Transaction.from(txLike); return { raw: tx, witnesses: tx.witnesses, }; }) .mapOut((tx) => Transaction.from({ ...tx.raw, witnesses: tx.witnesses }))) ], Transaction); export { Transaction }; /** * Calculate Nervos DAO profit between two blocks */ export function calcDaoProfit(profitableCapacity, depositHeaderLike, withdrawHeaderLike) { 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, withdrawHeader) { const depositEpoch = ClientBlockHeader.from(depositHeader).epoch; const withdrawEpoch = ClientBlockHeader.from(withdrawHeader).epoch; const intDiff = withdrawEpoch[0] - depositEpoch[0]; // deposit[1] withdraw[1] // ---------- <= ----------- // deposit[2] withdraw[2] if (intDiff % numFrom(180) !== numFrom(0) || depositEpoch[1] * withdrawEpoch[2] <= depositEpoch[2] * withdrawEpoch[1]) { return [ depositEpoch[0] + (intDiff / numFrom(180) + numFrom(1)) * numFrom(180), depositEpoch[1], depositEpoch[2], ]; } return [ depositEpoch[0] + (intDiff / numFrom(180)) * numFrom(180), depositEpoch[1], depositEpoch[2], ]; }