diff --git a/.changeset/polite-bats-shop.md b/.changeset/polite-bats-shop.md new file mode 100644 index 00000000..0ba17513 --- /dev/null +++ b/.changeset/polite-bats-shop.md @@ -0,0 +1,6 @@ +--- +"@ckb-ccc/core": patch +--- + +Add CKB_TX_MESSAGE_ALL: https://github.com/nervosnetwork/rfcs/pull/446 + diff --git a/packages/core/package.json b/packages/core/package.json index 60488f51..b080f490 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -78,4 +78,4 @@ "ws": "^8.18.0" }, "packageManager": "pnpm@10.8.1" -} \ No newline at end of file +} diff --git a/packages/core/src/ckb/transaction.ts b/packages/core/src/ckb/transaction.ts index 295f4449..cc136121 100644 --- a/packages/core/src/ckb/transaction.ts +++ b/packages/core/src/ckb/transaction.ts @@ -1137,6 +1137,24 @@ export class Transaction extends mol.Entity.Base< hasher.update(raw); } + private static hashBytesToHasher(bytes: HexLike, hasher: Hasher) { + const raw = bytesFrom(hexFrom(bytes)); + hasher.update(numToBytes(raw.length, 4)); + hasher.update(raw); + } + private static hashBytesOptToHasher( + bytes: HexLike | undefined | null, + hasher: Hasher, + ) { + if (bytes) { + const raw = bytesFrom(hexFrom(bytes)); + const moleculeBytes = mol.Bytes.encode(raw); + hasher.update(numToBytes(moleculeBytes.length, 4)); + hasher.update(moleculeBytes); + } else { + hasher.update(numToBytes(0, 4)); + } + } /** * Computes the signing hash information for a given script. * @@ -1193,6 +1211,83 @@ export class Transaction extends mol.Entity.Base< position, }; } + /** + * Computes the signing hash information for a given script, specified in the spec: + * https://github.com/nervosnetwork/rfcs/pull/446 + * + * @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.getTxMessageAll(scriptLike, client); + * if (signHashInfo) { + * console.log(signHashInfo.message); // Outputs the signing message + * console.log(signHashInfo.position); // Outputs the witness position + * } + * ``` + */ + async getTxMessageAll( + 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.inputs.length; i += 1) { + const { cellOutput, outputData } = await this.inputs[i].getCell(client); + hasher.update(cellOutput.toBytes()); + Transaction.hashBytesToHasher(outputData, hasher); + } + + 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; + } + if (i === position) { + // the first witness field in current script group + + // The spec requires that: + // The first witness field of the current running script group, must be a valid WitnessArgs structure serialized + // in the molecule serialization format, with compatible mode turned off. + // The molecule in ccc is by default in compatible mode off. + const witnessArgs = this.getWitnessArgsAt(i); + + Transaction.hashBytesOptToHasher(witnessArgs?.inputType, hasher); + Transaction.hashBytesOptToHasher(witnessArgs?.outputType, hasher); + } else { + // 1. Starting from the second witness field in current script group + // 2. Starting from the first witness that do not have an input cell of the same index + Transaction.hashBytesToHasher(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