diff --git a/.changeset/fuzzy-years-add.md b/.changeset/fuzzy-years-add.md new file mode 100644 index 000000000..29371a80e --- /dev/null +++ b/.changeset/fuzzy-years-add.md @@ -0,0 +1,6 @@ +--- +"@ckb-ccc/core": minor +--- + +feat(core): relax `@ccc.codec`'s type restriction + \ No newline at end of file diff --git a/.changeset/little-zebras-pump.md b/.changeset/little-zebras-pump.md new file mode 100644 index 000000000..a0c05a283 --- /dev/null +++ b/.changeset/little-zebras-pump.md @@ -0,0 +1,6 @@ +--- +"@ckb-ccc/core": minor +--- + +feat(core): multisig Signers + \ No newline at end of file diff --git a/.changeset/orange-goats-float.md b/.changeset/orange-goats-float.md new file mode 100644 index 000000000..0398b482b --- /dev/null +++ b/.changeset/orange-goats-float.md @@ -0,0 +1,6 @@ +--- +"@ckb-ccc/core": minor +--- + +feat(core): `Transaction.getWitnessArgsAtUnsafe` + \ No newline at end of file diff --git a/.changeset/vast-trees-film.md b/.changeset/vast-trees-film.md new file mode 100644 index 000000000..a2bd05a64 --- /dev/null +++ b/.changeset/vast-trees-film.md @@ -0,0 +1,6 @@ +--- +"@ckb-ccc/core": minor +--- + +feat(core): bump @noble packages + \ No newline at end of file diff --git a/packages/core/package.json b/packages/core/package.json index 56c3b8cd4..8b5e5a0b2 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -12,46 +12,42 @@ }, "sideEffects": false, "main": "./dist.commonjs/index.js", - "module": "./dist/index.js", + "module": "./dist/index.mjs", "exports": { ".": { - "import": "./dist/index.js", "require": "./dist.commonjs/index.js", - "default": "./dist.commonjs/index.js" + "import": "./dist/index.mjs" }, - "./barrel": { - "import": "./dist/barrel.js", - "require": "./dist.commonjs/barrel.js", - "default": "./dist.commonjs/barrel.js" + "./advanced": { + "require": "./dist.commonjs/advanced.js", + "import": "./dist/advanced.mjs" }, "./advancedBarrel": { - "import": "./dist/advancedBarrel.js", "require": "./dist.commonjs/advancedBarrel.js", - "default": "./dist.commonjs/advancedBarrel.js" + "import": "./dist/advancedBarrel.mjs" }, - "./advanced": { - "import": "./dist/advanced.js", - "require": "./dist.commonjs/advanced.js", - "default": "./dist.commonjs/advanced.js" - } + "./barrel": { + "require": "./dist.commonjs/barrel.js", + "import": "./dist/barrel.mjs" + }, + "./package.json": "./package.json" }, "scripts": { "test": "vitest", "test:ci": "vitest run", - "build": "rimraf ./dist && rimraf ./dist.commonjs && tsc && tsc --project tsconfig.commonjs.json && copyfiles -u 2 misc/basedirs/**/* .", + "build": "tsdown", "lint": "eslint ./src", "format": "prettier --write . && eslint --fix ./src" }, "devDependencies": { "@eslint/js": "^9.34.0", "@types/ws": "^8.18.1", - "copyfiles": "^2.4.1", "eslint": "^9.34.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-prettier": "^5.5.4", "prettier": "^3.6.2", "prettier-plugin-organize-imports": "^4.2.0", - "rimraf": "^6.0.1", + "tsdown": "0.19.0-beta.3", "typescript": "^5.9.2", "typescript-eslint": "^8.41.0", "vitest": "^3.2.4" @@ -61,9 +57,9 @@ }, "dependencies": { "@joyid/ckb": "^1.1.2", - "@noble/ciphers": "^0.5.3", - "@noble/curves": "^1.9.7", - "@noble/hashes": "^1.8.0", + "@noble/ciphers": "^2.1.1", + "@noble/curves": "^2.0.1", + "@noble/hashes": "^2.0.1", "bech32": "^2.0.0", "bs58check": "^4.0.0", "buffer": "^6.0.3", @@ -71,5 +67,6 @@ "isomorphic-ws": "^5.0.0", "ws": "^8.18.3" }, - "packageManager": "pnpm@10.8.1" + "packageManager": "pnpm@10.8.1", + "types": "./dist.commonjs/index.d.ts" } diff --git a/packages/core/src/ckb/transaction.ts b/packages/core/src/ckb/transaction.ts index 2cb60e906..bf11bb669 100644 --- a/packages/core/src/ckb/transaction.ts +++ b/packages/core/src/ckb/transaction.ts @@ -1768,10 +1768,29 @@ export class Transaction extends Entity.Base() { * * @example * ```typescript - * const witnessArgs = await tx.getWitnessArgsAt(0); + * const witnessArgs = tx.getWitnessArgsAt(0); * ``` */ getWitnessArgsAt(index: number): WitnessArgs | undefined { + try { + return this.getWitnessArgsAtUnsafe(index); + } catch (_) { + return undefined; + } + } + + /** + * Get witness at index as WitnessArgs, throw if failed to decode + * + * @param index - The index of the witness. + * @returns The witness parsed as WitnessArgs. + * + * @example + * ```typescript + * const witnessArgs = tx.getWitnessArgsAtUnsafe(0); + * ``` + */ + getWitnessArgsAtUnsafe(index: number): WitnessArgs | undefined { const rawWitness = this.witnesses[index]; return (rawWitness ?? "0x") !== "0x" ? WitnessArgs.fromBytes(rawWitness) diff --git a/packages/core/src/client/clientPublicMainnet.advanced.ts b/packages/core/src/client/clientPublicMainnet.advanced.ts index bdcec4cc1..e6b3259c1 100644 --- a/packages/core/src/client/clientPublicMainnet.advanced.ts +++ b/packages/core/src/client/clientPublicMainnet.advanced.ts @@ -54,6 +54,23 @@ export const MAINNET_SCRIPTS: Record = }, ], }, + [KnownScript.Secp256k1MultisigV2Beta]: { + codeHash: + "0xd1a9f877aed3f5e07cb9c52b61ab96d06f250ae6883cc7f0a2423db0976fc821", + hashType: "type", + cellDeps: [ + { + cellDep: { + outPoint: { + txHash: + "0x44be4f4feda80c0e41783ab10e191df3b2bb5c3731b0970c916dbec385dcdc60", + index: 0, + }, + depType: "depGroup", + }, + }, + ], + }, [KnownScript.Secp256k1MultisigV2]: { codeHash: "0x36c971b8d41fbd94aabca77dc75e826729ac98447b46f91e00796155dddb0d29", diff --git a/packages/core/src/client/clientPublicTestnet.advanced.ts b/packages/core/src/client/clientPublicTestnet.advanced.ts index 9453d2523..fe1e84555 100644 --- a/packages/core/src/client/clientPublicTestnet.advanced.ts +++ b/packages/core/src/client/clientPublicTestnet.advanced.ts @@ -54,6 +54,23 @@ export const TESTNET_SCRIPTS: Record = }, ], }, + [KnownScript.Secp256k1MultisigV2Beta]: { + codeHash: + "0x765b3ed6ae264b335d07e73ac332bf2c0f38f8d3340ed521cb447b4c42dd5f09", + hashType: "type", + cellDeps: [ + { + cellDep: { + outPoint: { + txHash: + "0xf2013f123b2cb745e3fdf5c935a3925647496f88090503eef58332a9245b4172", + index: 0, + }, + depType: "depGroup", + }, + }, + ], + }, [KnownScript.Secp256k1MultisigV2]: { codeHash: "0x36c971b8d41fbd94aabca77dc75e826729ac98447b46f91e00796155dddb0d29", diff --git a/packages/core/src/client/knownScript.ts b/packages/core/src/client/knownScript.ts index 90a1546fe..491da9989 100644 --- a/packages/core/src/client/knownScript.ts +++ b/packages/core/src/client/knownScript.ts @@ -5,6 +5,7 @@ export enum KnownScript { NervosDao = "NervosDao", Secp256k1Blake160 = "Secp256k1Blake160", Secp256k1Multisig = "Secp256k1Multisig", + Secp256k1MultisigV2Beta = "Secp256k1MultisigV2Beta", // Fix rare failing case (https://github.com/nervosnetwork/ckb-system-scripts/pull/98) Secp256k1MultisigV2 = "Secp256k1MultisigV2", // Enhanced since handling (https://github.com/nervosnetwork/ckb-system-scripts/pull/99) AnyoneCanPay = "AnyoneCanPay", TypeId = "TypeId", diff --git a/packages/core/src/codec/entity.ts b/packages/core/src/codec/entity.ts index d62a7a539..c44bcaf08 100644 --- a/packages/core/src/codec/entity.ts +++ b/packages/core/src/codec/entity.ts @@ -1,8 +1,7 @@ -import { Bytes, bytesEq, BytesLike } from "../bytes/index.js"; +import { Bytes, bytesEq, bytesFrom, BytesLike } from "../bytes/index.js"; import { hashCkb } from "../hasher/index.js"; import { Hex, hexFrom } from "../hex/index.js"; import { Constructor } from "../utils/index.js"; -import { Codec } from "./codec.js"; /** * The base class of CCC to create a serializable instance. This should be used with the {@link codec} decorator. @@ -172,7 +171,14 @@ export function codec< Encodable, TypeLike extends Encodable, Decoded extends TypeLike, ->(codec: Codec) { +>(codec: { + encode: (encodable: Encodable) => Bytes; + decode: ( + decodable: Bytes, + config?: { isExtraFieldIgnored?: boolean }, + ) => Decoded; + byteLength?: number; +}) { return function < Type extends TypeLike, ConstructorType extends Constructor & { @@ -191,12 +197,12 @@ export function codec< } if (Constructor.decode === undefined) { Constructor.decode = function (bytesLike: BytesLike) { - return Constructor.from(codec.decode(bytesLike)); + return Constructor.from(codec.decode(bytesFrom(bytesLike))); }; } if (Constructor.fromBytes === undefined) { Constructor.fromBytes = function (bytes: BytesLike) { - return Constructor.from(codec.decode(bytes)); + return Constructor.from(codec.decode(bytesFrom(bytes))); }; } diff --git a/packages/core/src/hasher/advanced.ts b/packages/core/src/hasher/advanced.ts index f3f4359bc..1344b964b 100644 --- a/packages/core/src/hasher/advanced.ts +++ b/packages/core/src/hasher/advanced.ts @@ -1 +1,3 @@ -export const CKB_BLAKE2B_PERSONAL = "ckb-default-hash"; +import { bytesFrom } from "../bytes"; + +export const CKB_BLAKE2B_PERSONAL = bytesFrom("ckb-default-hash", "utf8"); diff --git a/packages/core/src/hasher/hasherCkb.ts b/packages/core/src/hasher/hasherCkb.ts index c32ec569c..efb5ec8e0 100644 --- a/packages/core/src/hasher/hasherCkb.ts +++ b/packages/core/src/hasher/hasherCkb.ts @@ -1,9 +1,12 @@ -import { blake2b } from "@noble/hashes/blake2b"; +import { blake2b } from "@noble/hashes/blake2.js"; import { BytesLike, bytesFrom } from "../bytes/index.js"; import { Hex, hexFrom } from "../hex/index.js"; import { CKB_BLAKE2B_PERSONAL } from "./advanced.js"; import { Hasher } from "./hasher.js"; +export const HASH_CKB_LENGTH = 32; +export const HASH_CKB_SHORT_LENGTH = 20; + /** * @public */ @@ -73,7 +76,6 @@ export class HasherCkb implements Hasher { * const hash = hashCkb("some data"); // Outputs something like "0x..." * ``` */ - export function hashCkb(...data: BytesLike[]): Hex { const hasher = new HasherCkb(); data.forEach((d) => hasher.update(d)); diff --git a/packages/core/src/hasher/hasherKeecak256.ts b/packages/core/src/hasher/hasherKeecak256.ts index 88beef6ec..4660fbb3c 100644 --- a/packages/core/src/hasher/hasherKeecak256.ts +++ b/packages/core/src/hasher/hasherKeecak256.ts @@ -1,4 +1,4 @@ -import { keccak_256 } from "@noble/hashes/sha3"; +import { keccak_256 } from "@noble/hashes/sha3.js"; import { BytesLike, bytesFrom } from "../bytes/index.js"; import { Hex, hexFrom } from "../hex/index.js"; import { Hasher } from "./hasher.js"; diff --git a/packages/core/src/keystore/index.ts b/packages/core/src/keystore/index.ts index bd963af30..3ce3e042a 100644 --- a/packages/core/src/keystore/index.ts +++ b/packages/core/src/keystore/index.ts @@ -1,7 +1,7 @@ -import { ctr } from "@noble/ciphers/aes"; -import { scryptAsync } from "@noble/hashes/scrypt"; -import { keccak_256 } from "@noble/hashes/sha3"; -import { randomBytes } from "@noble/hashes/utils"; +import { ctr } from "@noble/ciphers/aes.js"; +import { scryptAsync } from "@noble/hashes/scrypt.js"; +import { keccak_256 } from "@noble/hashes/sha3.js"; +import { randomBytes } from "@noble/hashes/utils.js"; import { Bytes, BytesLike, bytesConcat, bytesFrom } from "../bytes/index.js"; import { hexFrom } from "../hex/index.js"; diff --git a/packages/core/src/molecule/codec.ts b/packages/core/src/molecule/codec.ts index ff09cab9a..8ae90fe6f 100644 --- a/packages/core/src/molecule/codec.ts +++ b/packages/core/src/molecule/codec.ts @@ -20,25 +20,25 @@ export { */ Codec, /** - * @deprecated Use ccc.CodecLike instead + * @deprecated Use ccc.codecUint instead */ - CodecLike, + codecUint as uint, /** - * @deprecated Use ccc.DecodedType instead + * @deprecated Use ccc.codecUintNumber instead */ - DecodedType, + codecUintNumber as uintNumber, /** - * @deprecated Use ccc.EncodableType instead + * @deprecated Use ccc.CodecLike instead */ - EncodableType, + type CodecLike, /** - * @deprecated Use ccc.codecUint instead + * @deprecated Use ccc.DecodedType instead */ - codecUint as uint, + type DecodedType, /** - * @deprecated Use ccc.codecUintNumber instead + * @deprecated Use ccc.EncodableType instead */ - codecUintNumber as uintNumber, + type EncodableType, } from "../codec/index.js"; function uint32To(numLike: NumLike) { diff --git a/packages/core/src/signer/btc/verify.ts b/packages/core/src/signer/btc/verify.ts index 2d7f7fb10..2907560a7 100644 --- a/packages/core/src/signer/btc/verify.ts +++ b/packages/core/src/signer/btc/verify.ts @@ -1,4 +1,4 @@ -import { secp256k1 } from "@noble/curves/secp256k1"; +import { secp256k1 } from "@noble/curves/secp256k1.js"; import { ripemd160 } from "@noble/hashes/legacy.js"; import { sha256 } from "@noble/hashes/sha2.js"; import bs58check from "bs58check"; @@ -98,5 +98,6 @@ export function verifyMessageBtcEcdsa( bytesFrom(rawSign), messageHashBtcEcdsa(challenge), bytesFrom(publicKey), + { prehash: false }, ); } diff --git a/packages/core/src/signer/ckb/index.ts b/packages/core/src/signer/ckb/index.ts index 61cec4a3a..5a9dc9281 100644 --- a/packages/core/src/signer/ckb/index.ts +++ b/packages/core/src/signer/ckb/index.ts @@ -1,5 +1,7 @@ +export * from "./secp256k1Signing.js"; export * from "./signerCkbPrivateKey.js"; export * from "./signerCkbPublicKey.js"; export * from "./signerCkbScriptReadonly.js"; -export * from "./verifyCkbSecp256k1.js"; +export * from "./signerMultisigCkbPrivateKey.js"; +export * from "./signerMultisigCkbReadonly.js"; export * from "./verifyJoyId.js"; diff --git a/packages/core/src/signer/ckb/secp256k1Signing.test.ts b/packages/core/src/signer/ckb/secp256k1Signing.test.ts new file mode 100644 index 000000000..87b339d49 --- /dev/null +++ b/packages/core/src/signer/ckb/secp256k1Signing.test.ts @@ -0,0 +1,85 @@ +import { secp256k1 } from "@noble/curves/secp256k1.js"; +import { describe, expect, it } from "vitest"; +import { ccc } from "../../index"; +import { + recoverMessageSecp256k1, + signMessageSecp256k1, + verifyMessageSecp256k1, +} from "./secp256k1Signing"; + +const client = new ccc.ClientPublicTestnet(); +const signer = new ccc.SignerCkbPrivateKey( + client, + "0x0123456789012345678901234567890123456789012345678901234567890123", +); + +describe("verifyMessageCkbSecp256k1", () => { + it("should verify a message signed by SignerCkbPrivateKey", async () => { + const message = "Hello CKB!"; + const { signature, identity } = await signer.signMessage(message); + + const isValid = ccc.verifyMessageCkbSecp256k1(message, signature, identity); + expect(isValid).toBe(true); + }); + + it("should fail to verify a message with a wrong signature", async () => { + const message = "Hello CKB!"; + const { identity } = await signer.signMessage(message); + + const signature = + "0x0010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000"; + + const isValid = ccc.verifyMessageCkbSecp256k1(message, signature, identity); + expect(isValid).toBe(false); + }); + + it("should fail to verify a message with a wrong public key", async () => { + const message = "Hello CKB!"; + const { signature } = await signer.signMessage(message); + + const identity = + "0x000000000000000000000000000000000000000000000000000000000000000000"; + + const isValid = ccc.verifyMessageCkbSecp256k1(message, signature, identity); + expect(isValid).toBe(false); + }); +}); + +describe("Secp256k1 Helpers", () => { + const privateKey = + "0x0123456789012345678901234567890123456789012345678901234567890123"; + const publicKey = ccc.hexFrom( + secp256k1.getPublicKey(ccc.bytesFrom(privateKey), true), + ); + const messageHash = + "0x1234567890123456789012345678901234567890123456789012345678901234"; + + it("should verifies a message", () => { + const isValid = verifyMessageSecp256k1( + messageHash, + "0xf71fd3e5b90289fa939bd3f3c0e263e8ea8e37550417344e58c9b1675084be456c506a30789a6ec98919e5458b3898199b560a41d5262cb18db37058cff339a300", + publicKey, + ); + expect(isValid).toBe(true); + }); + + it("should sign and verify a message hash", () => { + const signature = signMessageSecp256k1(messageHash, privateKey); + const isValid = verifyMessageSecp256k1(messageHash, signature, publicKey); + expect(isValid).toBe(true); + }); + + it("should recover the public key from the signature", () => { + const signature = signMessageSecp256k1(messageHash, privateKey); + const recovered = recoverMessageSecp256k1(messageHash, signature); + expect(recovered).toBe(publicKey); + }); + + it("should fail verification with wrong message", () => { + const signature = signMessageSecp256k1(messageHash, privateKey); + const wrongMessage = + "0x0000000000000000000000000000000000000000000000000000000000000000"; + const isValid = verifyMessageSecp256k1(wrongMessage, signature, publicKey); + expect(isValid).toBe(false); + }); +}); diff --git a/packages/core/src/signer/ckb/secp256k1Signing.ts b/packages/core/src/signer/ckb/secp256k1Signing.ts new file mode 100644 index 000000000..827b5e1e4 --- /dev/null +++ b/packages/core/src/signer/ckb/secp256k1Signing.ts @@ -0,0 +1,94 @@ +import { secp256k1 } from "@noble/curves/secp256k1.js"; +import { bytesConcat, bytesFrom, BytesLike } from "../../bytes/index.js"; +import { hashCkb } from "../../hasher/index.js"; +import { Hex, hexFrom } from "../../hex/index.js"; + +export const SECP256K1_SIGNATURE_LENGTH = 65; + +/** + * Sign a message using Secp256k1. + * + * @param message - The message to sign. + * @param privateKey - The private key. + * @returns The signature. + * @public + */ +export function signMessageSecp256k1( + message: BytesLike, + privateKey: BytesLike, +): Hex { + const signature = secp256k1.sign(bytesFrom(message), bytesFrom(privateKey), { + format: "recovered", + prehash: false, + }); + return hexFrom(bytesConcat(signature.slice(1), signature.slice(0, 1))); +} + +/** + * Verify a message using Secp256k1. + * + * @param message - The message to verify. + * @param signature - The signature. + * @param publicKey - The public key. + * @returns True if the signature is valid, false otherwise. + * @public + */ +export function verifyMessageSecp256k1( + message: BytesLike, + signature: BytesLike, + publicKey: BytesLike, +): boolean { + const signatureBytes = bytesFrom(signature); + return secp256k1.verify( + bytesConcat(signatureBytes.slice(64), signatureBytes.slice(0, 64)), + bytesFrom(message), + bytesFrom(publicKey), + { format: "recovered", prehash: false }, + ); +} + +/** + * Recover the public key from a Secp256k1 signature. + * + * @param message - The message. + * @param signature - The signature. + * @returns The recovered public key. + * @public + */ +export function recoverMessageSecp256k1( + message: BytesLike, + signature: BytesLike, +): Hex { + const signatureBytes = bytesFrom(signature); + return hexFrom( + secp256k1.recoverPublicKey( + bytesConcat(signatureBytes.slice(64), signatureBytes.slice(0, 64)), + bytesFrom(message), + { prehash: false }, + ), + ); +} + +/** + * @public + */ +export function messageHashCkbSecp256k1(message: string | BytesLike): Hex { + const msg = typeof message === "string" ? message : hexFrom(message); + const buffer = bytesFrom(`Nervos Message:${msg}`, "utf8"); + return hashCkb(buffer); +} + +/** + * @public + */ +export function verifyMessageCkbSecp256k1( + message: string | BytesLike, + signature: string, + publicKey: string, +): boolean { + return verifyMessageSecp256k1( + messageHashCkbSecp256k1(message), + signature, + publicKey, + ); +} diff --git a/packages/core/src/signer/ckb/signerCkbPrivateKey.ts b/packages/core/src/signer/ckb/signerCkbPrivateKey.ts index 31354a93c..4f6272129 100644 --- a/packages/core/src/signer/ckb/signerCkbPrivateKey.ts +++ b/packages/core/src/signer/ckb/signerCkbPrivateKey.ts @@ -1,11 +1,13 @@ -import { secp256k1 } from "@noble/curves/secp256k1"; -import { bytesConcat, bytesFrom, BytesLike } from "../../bytes/index.js"; +import { secp256k1 } from "@noble/curves/secp256k1.js"; +import { bytesFrom, BytesLike } from "../../bytes/index.js"; import { Transaction, TransactionLike, WitnessArgs } from "../../ckb/index.js"; import { Client } from "../../client/index.js"; import { Hex, hexFrom, HexLike } from "../../hex/index.js"; -import { numBeToBytes } from "../../num/index.js"; +import { + messageHashCkbSecp256k1, + signMessageSecp256k1, +} from "./secp256k1Signing.js"; import { SignerCkbPublicKey } from "./signerCkbPublicKey.js"; -import { messageHashCkbSecp256k1 } from "./verifyCkbSecp256k1.js"; /** * @public @@ -24,19 +26,7 @@ export class SignerCkbPrivateKey extends SignerCkbPublicKey { } async _signMessage(message: HexLike): Promise { - const signature = secp256k1.sign( - bytesFrom(message), - bytesFrom(this.privateKey), - ); - const { r, s, recovery } = signature; - - return hexFrom( - bytesConcat( - numBeToBytes(r, 32), - numBeToBytes(s, 32), - numBeToBytes(recovery, 1), - ), - ); + return signMessageSecp256k1(message, this.privateKey); } async signMessageRaw(message: string | BytesLike): Promise { diff --git a/packages/core/src/signer/ckb/signerCkbPublicKey.ts b/packages/core/src/signer/ckb/signerCkbPublicKey.ts index 20cf8f2cb..81855c34b 100644 --- a/packages/core/src/signer/ckb/signerCkbPublicKey.ts +++ b/packages/core/src/signer/ckb/signerCkbPublicKey.ts @@ -2,9 +2,10 @@ import { Address } from "../../address/index.js"; import { bytesFrom } from "../../bytes/index.js"; import { Script, Transaction, TransactionLike } from "../../ckb/index.js"; import { CellDepInfo, Client, KnownScript } from "../../client/index.js"; -import { hashCkb } from "../../hasher/index.js"; +import { HASH_CKB_SHORT_LENGTH, hashCkb } from "../../hasher/index.js"; import { Hex, HexLike, hexFrom } from "../../hex/index.js"; import { Signer, SignerSignType, SignerType } from "../signer/index.js"; +import { SECP256K1_SIGNATURE_LENGTH } from "./secp256k1Signing.js"; /** * @public @@ -47,7 +48,7 @@ export class SignerCkbPublicKey extends Signer { return Address.fromKnownScript( this.client, KnownScript.Secp256k1Blake160, - bytesFrom(hashCkb(this.publicKey)).slice(0, 20), + bytesFrom(hashCkb(this.publicKey)).slice(0, HASH_CKB_SHORT_LENGTH), ); } @@ -140,7 +141,11 @@ export class SignerCkbPublicKey extends Signer { await Promise.all( (await this.getRelatedScripts(tx)).map(async ({ script, cellDeps }) => { - await tx.prepareSighashAllWitness(script, 65, this.client); + await tx.prepareSighashAllWitness( + script, + SECP256K1_SIGNATURE_LENGTH, + this.client, + ); await tx.addCellDepInfos(this.client, cellDeps); }), ); diff --git a/packages/core/src/signer/ckb/signerMultisigCkb.test.ts b/packages/core/src/signer/ckb/signerMultisigCkb.test.ts new file mode 100644 index 000000000..e51e24bf6 --- /dev/null +++ b/packages/core/src/signer/ckb/signerMultisigCkb.test.ts @@ -0,0 +1,78 @@ +import { describe, expect, it } from "vitest"; +import { ccc } from "../../index.js"; + +const client = new ccc.ClientPublicTestnet(); + +describe("MultisigCkbWitness", () => { + it("should encode and decode correctly", () => { + const witness: ccc.MultisigCkbWitnessLike = { + publicKeys: [ + "0x024a501efd328e062c8675f2365970728c859c592beeefd6be8ece3d8d3c80fa80", + "0x024a501efd328e062c8675f2365970728c859c592beeefd6be8ece3d8d3c80fa81", + ], + threshold: 1, + mustMatch: 0, + signatures: [], + }; + + const encoded = ccc.MultisigCkbWitness.from(witness).toBytes(); + const decoded = ccc.MultisigCkbWitness.decode(encoded); + + expect(decoded.threshold).toBe(witness.threshold); + expect(decoded.mustMatch).toBe(witness.mustMatch); + expect(decoded.publicKeyHashes.length).toBe(witness.publicKeys.length); + }); + + it("should throw error for invalid threshold", () => { + expect(() => { + new ccc.MultisigCkbWitness([], 0, 0, []); + }).toThrow("threshold should be in range from 1 to public keys length"); + + expect(() => { + new ccc.MultisigCkbWitness([], 1, 0, []); + }).toThrow("threshold should be in range from 1 to public keys length"); + }); + + it("should throw error for invalid mustMatch", () => { + expect(() => { + new ccc.MultisigCkbWitness(["0x00"], 1, 2, []); + }).toThrow( + "mustMatch should be in range from 0 to min(public keys length, threshold)", + ); + }); + + it("should calculate scriptArgs correctly", () => { + const witness: ccc.MultisigCkbWitnessLike = { + publicKeys: [ + "0x024a501efd328e062c8675f2365970728c859c592beeefd6be8ece3d8d3c80fa80", + "0x024a501efd328e062c8675f2365970728c859c592beeefd6be8ece3d8d3c80fa81", + ], + threshold: 1, + mustMatch: 0, + }; + const multisigWitness = ccc.MultisigCkbWitness.from(witness); + const args = multisigWitness.scriptArgs(); + expect(args).toBeInstanceOf(Uint8Array); + expect(ccc.hexFrom(args)).toBe( + "0x6418f118e94d8dff7d9b0b59a4d837c4e201c5a9", + ); + }); +}); + +describe("SignerMultisigCkbReadonly", () => { + it("should initialize correctly", async () => { + const witness: ccc.MultisigCkbWitnessLike = { + publicKeys: [ + "0x024a501efd328e062c8675f2365970728c859c592beeefd6be8ece3d8d3c80fa80", + "0x024a501efd328e062c8675f2365970728c859c592beeefd6be8ece3d8d3c80fa81", + ], + threshold: 1, + mustMatch: 0, + }; + + const signer = new ccc.SignerMultisigCkbReadonly(client, witness); + + expect(await signer.getMemberCount()).toBe(2); + expect(await signer.getMemberThreshold()).toBe(1); + }); +}); diff --git a/packages/core/src/signer/ckb/signerMultisigCkbPrivateKey.ts b/packages/core/src/signer/ckb/signerMultisigCkbPrivateKey.ts new file mode 100644 index 000000000..3f532c096 --- /dev/null +++ b/packages/core/src/signer/ckb/signerMultisigCkbPrivateKey.ts @@ -0,0 +1,96 @@ +import { SinceLike, Transaction, TransactionLike } from "../../ckb/index.js"; +import { Client, KnownScript, ScriptInfoLike } from "../../client/index.js"; +import { Hex, hexFrom, HexLike } from "../../hex/index.js"; +import { + signMessageSecp256k1, + verifyMessageSecp256k1, +} from "./secp256k1Signing.js"; +import { SignerCkbPrivateKey } from "./signerCkbPrivateKey.js"; +import { + MultisigCkbWitnessLike, + SignerMultisigCkbReadonly, +} from "./signerMultisigCkbReadonly.js"; + +/** + * A class extending Signer that provides access to a CKB multisig script and supports signing operations. + * @public + */ +export class SignerMultisigCkbPrivateKey extends SignerMultisigCkbReadonly { + private readonly privateKey: Hex; + private readonly signer: SignerCkbPrivateKey; + + /** + * Creates an instance of SignerMultisigCkbPrivateKey. + * + * @param client - The client instance. + * @param privateKey - The private key. + * @param multisigInfo - The multisig information. + * @param options - The options. + */ + constructor( + client: Client, + privateKey: HexLike, + multisigInfo: MultisigCkbWitnessLike, + options?: { + since?: SinceLike | null; + scriptInfos?: (KnownScript | ScriptInfoLike)[] | null; + } | null, + ) { + super(client, multisigInfo, options); + + this.privateKey = hexFrom(privateKey); + this.signer = new SignerCkbPrivateKey(client, this.privateKey); + } + + /** + * Sign a transaction only (without preparing). + * + * @param txLike - The transaction to sign. + * @returns The signed transaction. + */ + async signOnlyTransaction(txLike: TransactionLike): Promise { + let tx = Transaction.from(txLike); + + for (const { script } of await this.scriptInfos) { + const info = await this.getSignInfo(tx, script); + if (!info) { + continue; + } + + // === Find a position for the signature === + tx = await this.prepareWitnessArgsAt( + tx, + info.position, + async (witness) => { + if ( + witness.signatures.some( + (sig) => + sig !== SignerMultisigCkbPrivateKey.EmptySignature && + verifyMessageSecp256k1( + info.message, + sig, + this.signer.publicKey, + ), + ) + ) { + // Has signed + return; + } + + const empty = witness.signatures.findIndex( + (sig) => sig === SignerMultisigCkbPrivateKey.EmptySignature, + ); + if (empty === -1) { + return; + } + + const signature = signMessageSecp256k1(info.message, this.privateKey); + + witness.signatures[empty] = signature; + }, + ); + } + + return tx; + } +} diff --git a/packages/core/src/signer/ckb/signerMultisigCkbReadonly.ts b/packages/core/src/signer/ckb/signerMultisigCkbReadonly.ts new file mode 100644 index 000000000..ee58d3366 --- /dev/null +++ b/packages/core/src/signer/ckb/signerMultisigCkbReadonly.ts @@ -0,0 +1,576 @@ +import { Address } from "../../address/index.js"; +import { Bytes, bytesConcat, bytesFrom } from "../../bytes/index.js"; +import { + Script, + ScriptLike, + Since, + SinceLike, + Transaction, + TransactionLike, + WitnessArgs, + WitnessArgsLike, +} from "../../ckb/index.js"; +import { + CellDepInfo, + CellDepInfoLike, + Client, + KnownScript, + ScriptInfo, + ScriptInfoLike, +} from "../../client/index.js"; +import { codec, Entity } from "../../codec/index.js"; +import { HASH_CKB_SHORT_LENGTH, hashCkb } from "../../hasher/index.js"; +import { Hex, hexFrom, HexLike } from "../../hex/index.js"; +import { numFrom, NumLike, numToBytes } from "../../num/index.js"; +import { apply, reduceAsync } from "../../utils/index.js"; +import { SignerMultisig, SignerSignType, SignerType } from "../signer/index.js"; +import { + recoverMessageSecp256k1, + SECP256K1_SIGNATURE_LENGTH, +} from "./secp256k1Signing.js"; + +export type MultisigCkbWitnessLike = ( + | { + publicKeyHashes: HexLike[]; + publicKeys?: undefined | null; + } + | { + publicKeyHashes?: undefined | null; + publicKeys: HexLike[]; + } +) & { + threshold: NumLike; + mustMatch?: NumLike | null; + signatures?: HexLike[] | null; +}; + +/** + * A class representing multisig information, holding information ingredients and containing utilities. + * @public + */ +@codec({ + encode: (encodable: MultisigCkbWitness) => { + const { publicKeyHashes, threshold, mustMatch, signatures } = + MultisigCkbWitness.from(encodable); + + if ( + signatures.some((s) => s.length !== SECP256K1_SIGNATURE_LENGTH * 2 + 2) + ) { + throw Error("MultisigCkbWitness: invalid signature length"); + } + if ( + publicKeyHashes.some((s) => s.length !== HASH_CKB_SHORT_LENGTH * 2 + 2) + ) { + throw Error("MultisigCkbWitness: invalid public key hash length"); + } + + return bytesConcat( + "0x00", + numToBytes(mustMatch ?? 0), + numToBytes(threshold), + numToBytes(publicKeyHashes.length), + ...publicKeyHashes, + ...signatures, + ); + }, + decode: (raw: Bytes) => { + const [ + _reserved, + mustMatch, + threshold, + publicKeyHashesLength, + ...rawKeyAndSignatures + ] = raw; + + if ( + rawKeyAndSignatures.length < + publicKeyHashesLength * HASH_CKB_SHORT_LENGTH + ) { + throw Error("MultisigCkbWitness: invalid public key hashes length"); + } + + const signatures = rawKeyAndSignatures.slice( + publicKeyHashesLength * HASH_CKB_SHORT_LENGTH, + ); + + return MultisigCkbWitness.from({ + publicKeyHashes: Array.from(new Array(publicKeyHashesLength), (_, i) => + hexFrom( + rawKeyAndSignatures.slice( + i * HASH_CKB_SHORT_LENGTH, + (i + 1) * HASH_CKB_SHORT_LENGTH, + ), + ), + ), + threshold: numFrom(threshold), + mustMatch: numFrom(mustMatch), + signatures: Array.from( + new Array(Math.floor(signatures.length / SECP256K1_SIGNATURE_LENGTH)), + (_, i) => + hexFrom( + signatures.slice( + i * SECP256K1_SIGNATURE_LENGTH, + (i + 1) * SECP256K1_SIGNATURE_LENGTH, + ), + ), + ), + }); + }, +}) +export class MultisigCkbWitness extends Entity.Base< + MultisigCkbWitnessLike, + MultisigCkbWitness +>() { + /** + * @param publicKeyHashes - The public key hashes. + * @param threshold - The threshold. + * @param mustMatch - The number of signatures that must match. + * @param signatures - The signatures. + */ + constructor( + public publicKeyHashes: Hex[], + public threshold: number, + public mustMatch: number, + public signatures: Hex[], + ) { + super(); + + const keysLength = publicKeyHashes.length; + + if (threshold <= 0 || threshold > keysLength) { + throw new Error( + "threshold should be in range from 1 to public keys length", + ); + } + if (mustMatch < 0 || mustMatch > Math.min(keysLength, threshold)) { + throw new Error( + "mustMatch should be in range from 0 to min(public keys length, threshold)", + ); + } + if (keysLength > 255) { + throw new Error("public keys length should be less than 256"); + } + } + + /** + * Create a MultisigCkbWitness from a MultisigCkbWitnessLike. + * + * @param witness - The witness like object. + * @returns The MultisigCkbWitness. + */ + static from(witness: MultisigCkbWitnessLike): MultisigCkbWitness { + const publicKeyHashes = (() => { + if (witness.publicKeyHashes) { + return witness.publicKeyHashes; + } + return witness.publicKeys.map((k) => hashCkb(k).slice(0, 42)); + })(); + + return new MultisigCkbWitness( + publicKeyHashes.map(hexFrom), + Number(numFrom(witness.threshold)), + Number(numFrom(witness.mustMatch ?? 0)), + witness.signatures?.map(hexFrom) ?? [], + ); + } + + /** + * Get the script args of the multisig script. + * + * @param since - The since value. + * @returns The script args. + */ + scriptArgs(since?: SinceLike | null): Bytes { + const hash = hashCkb(this.toBytes()).slice(0, 42); + + if (since != null) { + return bytesConcat(hash, Since.from(since).toBytes()); + } + + return bytesFrom(hash); + } + + /** + * Check if the multisig info is equal to another. + * + * @param otherLike - The other multisig info. + * @returns True if the multisig info is equal, false otherwise. + */ + eqInfo(otherLike: MultisigCkbWitnessLike): boolean { + const other = MultisigCkbWitness.from(otherLike); + return ( + this.publicKeyHashes.length === other.publicKeyHashes.length && + this.publicKeyHashes.every((h, i) => h === other.publicKeyHashes[i]) && + this.threshold === other.threshold && + this.mustMatch === other.mustMatch + ); + } +} + +/** + * A class extending Signer that provides access to a CKB multisig script. + * This class does not support signing operations. + * @public + */ +export class SignerMultisigCkbReadonly extends SignerMultisig { + static EmptySignature = hexFrom("00".repeat(SECP256K1_SIGNATURE_LENGTH)); + + get type(): SignerType { + return SignerType.CKB; + } + + get signType(): SignerSignType { + return SignerSignType.Unknown; + } + + public readonly multisigInfo: MultisigCkbWitness; + + public readonly since?: Since; + public readonly scriptInfos: Promise< + { + script: Script; + cellDeps: CellDepInfo[]; + }[] + >; + + /** + * Creates an instance of SignerMultisigCkbReadonly. + * + * @param client - The client instance. + * @param multisigInfoLike - The multisig information. + * @param options - The options. + */ + constructor( + client: Client, + multisigInfoLike: MultisigCkbWitnessLike, + options?: { + since?: SinceLike | null; + scriptInfos?: (KnownScript | ScriptInfoLike)[] | null; + } | null, + ) { + super(client); + + this.multisigInfo = MultisigCkbWitness.from(multisigInfoLike); + this.since = apply(Since.from, options?.since); + + const args = this.multisigInfo.scriptArgs(this.since); + this.scriptInfos = Promise.all( + ( + options?.scriptInfos ?? [ + KnownScript.Secp256k1MultisigV2, + KnownScript.Secp256k1MultisigV2Beta, + KnownScript.Secp256k1Multisig, + ] + ).map(async (v) => + typeof v === "string" ? client.getKnownScript(v) : ScriptInfo.from(v), + ), + ).then((infos) => + infos.map((i) => ({ + script: Script.from({ ...i, args }), + cellDeps: i.cellDeps, + })), + ); + } + + /** + * Get the number of members in the multisig script. + * + * @returns The number of members. + */ + async getMemberCount() { + return this.multisigInfo.publicKeyHashes.length; + } + + /** + * Get the threshold of the multisig script. + * + * @returns The threshold. + */ + async getMemberThreshold() { + return this.multisigInfo.threshold; + } + + async connect(): Promise {} + + async isConnected(): Promise { + return true; + } + + async getInternalAddress(): Promise { + return this.getRecommendedAddress(); + } + + async getAddressObjs(): Promise { + return (await this.scriptInfos).map(({ script }) => + Address.fromScript(script, this.client), + ); + } + + /** + * Decode the witness args at a specific index. + * + * @param txLike - The transaction. + * @param index - The index of the witness args. + * @returns The decoded MultisigCkbWitness. + */ + decodeWitnessArgsAt( + txLike: TransactionLike, + index: number, + ): MultisigCkbWitness | undefined { + const tx = Transaction.from(txLike); + + return this.decodeWitnessArgs(tx.getWitnessArgsAt(index)); + } + + /** + * Decode the witness args. + * + * @param witnessLike - The witness args like object. + * @returns The decoded MultisigCkbWitness. + */ + decodeWitnessArgs( + witnessLike?: WitnessArgsLike | null, + ): MultisigCkbWitness | undefined { + if (!witnessLike) { + return; + } + const witness = WitnessArgs.from(witnessLike); + + if (witness.lock == null) { + return; + } + + try { + const decoded = MultisigCkbWitness.decode(witness.lock); + if (decoded.eqInfo(this.multisigInfo)) { + return decoded; + } + } catch (_) { + // Returns undefined for invalid data + } + } + + /** + * Prepare the witness args at a specific index. + * + * @param txLike - The transaction. + * @param index - The index of the witness args. + * @param transformer - The transformer function. + * @returns The prepared transaction. + */ + async prepareWitnessArgsAt( + txLike: TransactionLike, + index: number, + transformer?: + | (( + witness: MultisigCkbWitness, + witnessArgs: WitnessArgs, + ) => + | MultisigCkbWitnessLike + | undefined + | null + | void + | Promise) + | null, + ): Promise { + const tx = Transaction.from(txLike); + + const witnessArgs = tx.getWitnessArgsAt(index) ?? WitnessArgs.from({}); + const multisigWitness = + this.decodeWitnessArgs(witnessArgs) ?? this.multisigInfo.clone(); + + multisigWitness.signatures = multisigWitness.signatures.slice( + 0, + this.multisigInfo.threshold, + ); + multisigWitness.signatures.push( + ...Array.from( + new Array( + this.multisigInfo.threshold - multisigWitness.signatures.length, + ), + () => SignerMultisigCkbReadonly.EmptySignature, + ), + ); + + witnessArgs.lock = MultisigCkbWitness.from( + (await transformer?.(multisigWitness, witnessArgs)) ?? multisigWitness, + ).toHex(); + tx.setWitnessArgsAt(index, witnessArgs); + + return tx; + } + + /** + * Prepare multisig witness, if the existence of multisig witness is detected, nothing happens + * + * @param txLike - The transaction to prepare. + * @param scriptLike - The script to prepare. + * @returns A promise that resolves to the prepared transaction + */ + async prepareTransactionOneScript( + txLike: TransactionLike, + script: ScriptLike, + cellDeps: CellDepInfoLike[], + ) { + const tx = Transaction.from(txLike); + const position = await tx.findInputIndexByLock(script, this.client); + if (position === undefined) { + return tx; + } + + await tx.addCellDepInfos(this.client, cellDeps); + return this.prepareWitnessArgsAt(tx, position); + } + + /** + * Prepare transaction for multisig witness and adding related cell deps + * + * @param txLike - The transaction to prepare. + * @returns A promise that resolves to the prepared transaction + */ + async prepareTransaction(txLike: TransactionLike): Promise { + return await reduceAsync( + await this.scriptInfos, + (tx, { script, cellDeps }) => + this.prepareTransactionOneScript(tx, script, cellDeps), + Transaction.from(txLike), + ); + } + + /** + * Get the number of signatures in the transaction. + * + * @param txLike - The transaction. + * @returns The number of signatures. + */ + async getSignaturesCount( + txLike: TransactionLike, + ): Promise { + const tx = Transaction.from(txLike); + let minSignaturesCount = undefined; + + for (const { script } of await this.scriptInfos) { + const index = await tx.findInputIndexByLock(script, this.client); + if (index === undefined) { + continue; + } + + const multisigWitness = this.decodeWitnessArgsAt(tx, index); + + if (!multisigWitness) { + minSignaturesCount = 0; + } else { + minSignaturesCount = Math.min( + minSignaturesCount ?? 256, + multisigWitness.signatures.reduce( + (acc, s) => + acc + (s === SignerMultisigCkbReadonly.EmptySignature ? 0 : 1), + 0, + ), + ); + } + } + + return minSignaturesCount; + } + + /** + * Check if the transaction needs more signatures + * + * @param txLike - The transaction to check. + * @returns A promise that resolves to true if the multisig witness is fulfilled, false otherwise. + */ + async needMoreSignatures(txLike: TransactionLike): Promise { + const count = await this.getSignaturesCount(txLike); + if (count == null) { + return false; + } + return count < (await this.getMemberThreshold()); + } + + /** + * Get the sign info for a script. + * + * @param txLike - The transaction. + * @param script - The script. + * @returns The sign info. + */ + async getSignInfo( + txLike: TransactionLike, + script: ScriptLike, + ): Promise<{ message: Hex; position: number } | undefined> { + const tx = Transaction.from(txLike); + + const position = await tx.findInputIndexByLock(script, this.client); + if (position == null) { + return; + } + + // === Replace the witness with a dummy one === + const witness = tx.getWitnessArgsAt(position) ?? WitnessArgs.from({}); + witness.lock = MultisigCkbWitness.from({ + ...this.multisigInfo, + signatures: Array.from( + new Array(this.multisigInfo.threshold), + () => SignerMultisigCkbReadonly.EmptySignature, + ), + }).toHex(); + + const clonedTx = tx.clone(); + clonedTx.setWitnessArgsAt(position, witness); + // === Replace the witness with a dummy one === + + return clonedTx.getSignHashInfo(script, this.client); + } + + /** + * Aggregate transactions. + * + * @param txs - The transactions to aggregate. + * @returns The aggregated transaction. + */ + async aggregateTransactions(txs: TransactionLike[]): Promise { + if (txs.length === 0) { + throw Error("No transaction to aggregate"); + } + + let res = Transaction.from(txs[0]); + for (const { script } of await this.scriptInfos) { + const info = await this.getSignInfo(res, script); + if (info === undefined) { + continue; + } + + const signatures = new Map(); + for (const txLike of txs) { + const tx = Transaction.from(txLike); + const multisigWitness = this.decodeWitnessArgsAt(tx, info.position); + + if (!multisigWitness) { + continue; + } + + for (const sig of multisigWitness.signatures) { + try { + signatures.set(recoverMessageSecp256k1(info.message, sig), sig); + } catch (_) { + // Ignore invalid signatures + } + if (signatures.size >= this.multisigInfo.threshold) { + break; + } + } + + if (signatures.size >= this.multisigInfo.threshold) { + break; + } + } + + res = await this.prepareWitnessArgsAt(res, info.position, (witness) => { + witness.signatures = Array.from(signatures.values()); + }); + } + + return res; + } +} diff --git a/packages/core/src/signer/ckb/verifyCkbSecp256k1.test.ts b/packages/core/src/signer/ckb/verifyCkbSecp256k1.test.ts deleted file mode 100644 index 2a2d176d5..000000000 --- a/packages/core/src/signer/ckb/verifyCkbSecp256k1.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { ccc } from "../../index"; - -const client = new ccc.ClientPublicTestnet(); -const signer = new ccc.SignerCkbPrivateKey( - client, - "0x0123456789012345678901234567890123456789012345678901234567890123", -); - -describe("verifyMessageCkbSecp256k1", () => { - it("should verify a message signed by SignerCkbPrivateKey", async () => { - const message = "Hello CKB!"; - const { signature, identity } = await signer.signMessage(message); - - const isValid = ccc.verifyMessageCkbSecp256k1(message, signature, identity); - expect(isValid).toBe(true); - }); - - it("should fail to verify a message with a wrong signature", async () => { - const message = "Hello CKB!"; - const { identity } = await signer.signMessage(message); - - const signature = - "0x0010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000"; - - const isValid = ccc.verifyMessageCkbSecp256k1(message, signature, identity); - expect(isValid).toBe(false); - }); - - it("should fail to verify a message with a wrong public key", async () => { - const message = "Hello CKB!"; - const { signature } = await signer.signMessage(message); - - const identity = - "0x000000000000000000000000000000000000000000000000000000000000000000"; - - const isValid = ccc.verifyMessageCkbSecp256k1(message, signature, identity); - expect(isValid).toBe(false); - }); -}); diff --git a/packages/core/src/signer/ckb/verifyCkbSecp256k1.ts b/packages/core/src/signer/ckb/verifyCkbSecp256k1.ts deleted file mode 100644 index c5e268e5c..000000000 --- a/packages/core/src/signer/ckb/verifyCkbSecp256k1.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { secp256k1 } from "@noble/curves/secp256k1"; -import { BytesLike, bytesFrom } from "../../bytes/index.js"; -import { hashCkb } from "../../hasher/index.js"; -import { Hex, hexFrom } from "../../hex/index.js"; -import { numFrom } from "../../num/index.js"; - -/** - * @public - */ -export function messageHashCkbSecp256k1(message: string | BytesLike): Hex { - const msg = typeof message === "string" ? message : hexFrom(message); - const buffer = bytesFrom(`Nervos Message:${msg}`, "utf8"); - return hashCkb(buffer); -} - -/** - * @public - */ -export function verifyMessageCkbSecp256k1( - message: string | BytesLike, - signature: string, - publicKey: string, -): boolean { - const signatureBytes = bytesFrom(signature); - return secp256k1.verify( - new secp256k1.Signature( - numFrom(signatureBytes.slice(0, 32)), - numFrom(signatureBytes.slice(32, 64)), - ) - .addRecoveryBit(Number(numFrom(signatureBytes.slice(64, 65)))) - .toBytes(), - bytesFrom(messageHashCkbSecp256k1(message)), - bytesFrom(publicKey), - ); -} diff --git a/packages/core/src/signer/doge/signerDogePrivateKey.ts b/packages/core/src/signer/doge/signerDogePrivateKey.ts index ad5e36d00..b1c2988a8 100644 --- a/packages/core/src/signer/doge/signerDogePrivateKey.ts +++ b/packages/core/src/signer/doge/signerDogePrivateKey.ts @@ -1,4 +1,4 @@ -import { secp256k1 } from "@noble/curves/secp256k1"; +import { secp256k1 } from "@noble/curves/secp256k1.js"; import { Bytes, bytesConcat, @@ -87,10 +87,13 @@ export class SignerDogePrivateKey extends SignerDoge { const signature = secp256k1.sign( messageHashDogeEcdsa(challenge), this.privateKey, + { + format: "recovered", + prehash: false, + }, ); - return bytesTo( - bytesConcat([31 + signature.recovery], signature.toCompactRawBytes()), + bytesConcat([31 + Number(signature[0])], signature.slice(1)), "base64", ); } diff --git a/packages/core/src/signer/doge/verify.ts b/packages/core/src/signer/doge/verify.ts index b9d586bfd..561be254f 100644 --- a/packages/core/src/signer/doge/verify.ts +++ b/packages/core/src/signer/doge/verify.ts @@ -1,4 +1,4 @@ -import { secp256k1 } from "@noble/curves/secp256k1"; +import { secp256k1 } from "@noble/curves/secp256k1.js"; import { Bytes, bytesFrom, BytesLike } from "../../bytes/index.js"; import { hexFrom } from "../../hex/index.js"; import { @@ -38,18 +38,17 @@ export function verifyMessageDogeEcdsa( const challenge = typeof message === "string" ? message : hexFrom(message).slice(2); const signatureBytes = bytesFrom(signature, "base64"); - const recoveryBit = signatureBytes[0]; - const rawSign = signatureBytes.slice(1); - - const sig = secp256k1.Signature.fromCompact( - hexFrom(rawSign).slice(2), - ).addRecoveryBit(recoveryBit - 31); + signatureBytes[0] -= 31; return ( btcPublicKeyFromP2pkhAddress(address) === hexFrom( btcEcdsaPublicKeyHash( - sig.recoverPublicKey(messageHashDogeEcdsa(challenge)).toHex(), + secp256k1.recoverPublicKey( + signatureBytes, + messageHashDogeEcdsa(challenge), + { prehash: false }, + ), ), ) ); diff --git a/packages/core/src/signer/nostr/signerNostrPrivateKey.ts b/packages/core/src/signer/nostr/signerNostrPrivateKey.ts index b299596fe..97fe90603 100644 --- a/packages/core/src/signer/nostr/signerNostrPrivateKey.ts +++ b/packages/core/src/signer/nostr/signerNostrPrivateKey.ts @@ -1,7 +1,8 @@ -import { schnorr } from "@noble/curves/secp256k1"; +import { schnorr } from "@noble/curves/secp256k1.js"; import { bech32 } from "bech32"; +import { Bytes, bytesFrom, BytesLike } from "../../bytes/index.js"; import { Client } from "../../client/index.js"; -import { Hex, hexFrom, HexLike } from "../../hex/index.js"; +import { hexFrom } from "../../hex/index.js"; import { NostrEvent } from "./signerNostr.js"; import { SignerNostrPublicKeyReadonly } from "./signerNostrPublicKeyReadonly.js"; import { nostrEventHash } from "./verify.js"; @@ -11,22 +12,22 @@ import { nostrEventHash } from "./verify.js"; * Support nsec and hex format */ export class SignerNostrPrivateKey extends SignerNostrPublicKeyReadonly { - private readonly privateKey: Hex; + private readonly privateKey: Bytes; - constructor(client: Client, privateKeyLike: HexLike) { + constructor(client: Client, privateKeyLike: BytesLike) { const privateKey = (() => { if ( typeof privateKeyLike === "string" && privateKeyLike.startsWith("nsec") ) { const { words } = bech32.decode(privateKeyLike); - return hexFrom(bech32.fromWords(words)); + return bytesFrom(bech32.fromWords(words)); } - return hexFrom(privateKeyLike); + return bytesFrom(privateKeyLike); })(); - super(client, schnorr.getPublicKey(privateKey.slice(2))); + super(client, schnorr.getPublicKey(privateKey)); this.privateKey = privateKey; } @@ -34,7 +35,7 @@ export class SignerNostrPrivateKey extends SignerNostrPublicKeyReadonly { async signNostrEvent(event: NostrEvent): Promise> { const pubkey = (await this.getNostrPublicKey()).slice(2); const eventHash = nostrEventHash({ ...event, pubkey }); - const signature = schnorr.sign(eventHash, this.privateKey.slice(2)); + const signature = schnorr.sign(eventHash, this.privateKey); return { ...event, diff --git a/packages/core/src/signer/nostr/verify.ts b/packages/core/src/signer/nostr/verify.ts index 879de0a70..da2db9b7e 100644 --- a/packages/core/src/signer/nostr/verify.ts +++ b/packages/core/src/signer/nostr/verify.ts @@ -1,4 +1,4 @@ -import { schnorr } from "@noble/curves/secp256k1"; +import { schnorr } from "@noble/curves/secp256k1.js"; import { sha256 } from "@noble/hashes/sha2.js"; import { bech32 } from "bech32"; import { Bytes, BytesLike, bytesFrom } from "../../bytes/index.js"; @@ -65,7 +65,7 @@ export function verifyMessageNostrEvent( const eventHash = nostrEventHash({ ...event, pubkey }); try { - return schnorr.verify(hexFrom(signature).slice(2), eventHash, pubkey); + return schnorr.verify(bytesFrom(signature), eventHash, bytesFrom(pubkey)); } catch (_) { return false; } diff --git a/packages/core/src/signer/signer/index.ts b/packages/core/src/signer/signer/index.ts index a2d0504b7..b3b694b1f 100644 --- a/packages/core/src/signer/signer/index.ts +++ b/packages/core/src/signer/signer/index.ts @@ -11,7 +11,7 @@ import { import { Hex } from "../../hex/index.js"; import { Num } from "../../num/index.js"; import { verifyMessageBtcEcdsa } from "../btc/verify.js"; -import { verifyMessageCkbSecp256k1 } from "../ckb/verifyCkbSecp256k1.js"; +import { verifyMessageCkbSecp256k1 } from "../ckb/secp256k1Signing.js"; import { verifyMessageJoyId } from "../ckb/verifyJoyId.js"; import { verifyMessageDogeEcdsa } from "../doge/verify.js"; import { verifyMessageEvmPersonal } from "../evm/verify.js"; @@ -491,6 +491,60 @@ export abstract class Signer { } } +/** + * An abstract class representing a multisig signer. + * @public + */ +export abstract class SignerMultisig extends Signer { + /** + * Get the number of members in the multisig script. + * @returns The number of members. + */ + abstract getMemberCount(): Promise; + + /** + * Get the threshold of the multisig script. + * @returns The threshold. + */ + abstract getMemberThreshold(): Promise; + + /** + * Get the number of signatures in the transaction. + * @param _ - The transaction. + * @returns The number of signatures. + */ + abstract getSignaturesCount(_: TransactionLike): Promise; + + /** + * Check if the transaction needs more signatures + * + * @param txLike - The transaction to check. + * @returns A promise that resolves to true if the multisig witness is fulfilled, false otherwise. + */ + abstract needMoreSignatures(_: TransactionLike): Promise; + + /** + * Aggregate transactions. + * @param _ - The transactions to aggregate. + * @returns The aggregated transaction. + */ + abstract aggregateTransactions(_: TransactionLike[]): Promise; + + /** + * Send a transaction. + * @param tx - The transaction to send. + * @returns The transaction hash. + */ + async sendTransaction(tx: TransactionLike): Promise { + const signedTx = await this.signTransaction(tx); + if (await this.needMoreSignatures(signedTx)) { + throw Error("Not enough signatures"); + } + + return this.client.sendTransaction(signedTx); + } +} + /** * A class representing information about a signer, including its type and the signer instance. * @public diff --git a/packages/core/tsdown.config.mts b/packages/core/tsdown.config.mts new file mode 100644 index 000000000..1afef7d20 --- /dev/null +++ b/packages/core/tsdown.config.mts @@ -0,0 +1,38 @@ +import { defineConfig } from "tsdown"; + +const common = { + minify: true, + dts: true, + platform: "neutral" as const, + exports: true, +}; + +const entry = { + index: "src/index.ts", + barrel: "src/barrel.ts", + advanced: "src/advanced.ts", + advancedBarrel: "src/advancedBarrel.ts", +} as const; + +export default defineConfig( + ( + [ + { + entry, + format: "esm", + copy: "./misc/basedirs/dist/*", + }, + { + entry, + noExternal: [ + "@noble/curves/*", + "@noble/hashes/*", + "@noble/ciphers/*", + ] as string[], + format: "cjs", + outDir: "dist.commonjs", + copy: "./misc/basedirs/dist.commonjs/*", + }, + ] as const + ).map((c) => ({ ...c, ...common })), +); diff --git a/packages/docs/docs/code-examples.md b/packages/docs/docs/code-examples.md index 850efdb55..46d161495 100644 --- a/packages/docs/docs/code-examples.md +++ b/packages/docs/docs/code-examples.md @@ -28,4 +28,11 @@ That's it! The transaction is sent. - [Use all supported wallets in custom UI.](https://live.ckbccc.com/?src=https://raw.githubusercontent.com/ckb-devrel/ccc/refs/heads/master/packages/examples/src/customUiWithController.ts) - [Sign and verify any message.](https://live.ckbccc.com/?src=https://raw.githubusercontent.com/ckb-devrel/ccc/refs/heads/master/packages/examples/src/sign.ts) - [Transfer all native CKB token.](https://live.ckbccc.com/?src=https://raw.githubusercontent.com/ckb-devrel/ccc/refs/heads/master/packages/examples/src/transferAll.ts) -- [Transfer UDT token.](https://live.ckbccc.com/?src=https://raw.githubusercontent.com/ckb-devrel/ccc/refs/heads/master/packages/examples/src/transferUdt.ts) \ No newline at end of file +- [Transfer UDT token.](https://live.ckbccc.com/?src=https://raw.githubusercontent.com/ckb-devrel/ccc/refs/heads/master/packages/examples/src/transferUdt.ts) +- [Create a DID on CKB.](https://live.ckbccc.com/?src=https://raw.githubusercontent.com/ckb-devrel/ccc/refs/heads/master/packages/examples/src/createDid.ts) +- [Create a DID with Local ID.](https://live.ckbccc.com/?src=https://raw.githubusercontent.com/ckb-devrel/ccc/refs/heads/master/packages/examples/src/createDidWithLocalId.ts) +- [Destroy a DID on CKB.](https://live.ckbccc.com/?src=https://raw.githubusercontent.com/ckb-devrel/ccc/refs/heads/master/packages/examples/src/destroyDid.ts) +- [Transfer a DID on CKB.](https://live.ckbccc.com/?src=https://raw.githubusercontent.com/ckb-devrel/ccc/refs/heads/master/packages/examples/src/transferDid.ts) +- [Transfer from a multisig address.](https://live.ckbccc.com/?src=https://raw.githubusercontent.com/ckb-devrel/ccc/refs/heads/master/packages/examples/src/transferFromMultisig.ts) +- [Transfer from a multisig address with aggregated transactions.](https://live.ckbccc.com/?src=https://raw.githubusercontent.com/ckb-devrel/ccc/refs/heads/master/packages/examples/src/transferFromMultisigAggregateTxs.ts) +- [Transfer to a multisig address.](https://live.ckbccc.com/?src=https://raw.githubusercontent.com/ckb-devrel/ccc/refs/heads/master/packages/examples/src/transferToMultisig.ts) \ No newline at end of file diff --git a/packages/examples/package.json b/packages/examples/package.json index bac19e45d..36c459a77 100644 --- a/packages/examples/package.json +++ b/packages/examples/package.json @@ -32,8 +32,8 @@ "dependencies": { "@ckb-ccc/ccc": "workspace:*", "@ckb-ccc/playground": "file:src/playground", - "@noble/curves": "^1.9.7", - "@noble/hashes": "^1.8.0" + "@noble/curves": "^2.0.1", + "@noble/hashes": "^2.0.1" }, "packageManager": "pnpm@10.8.1" } diff --git a/packages/examples/src/createDidWithLocalId.ts b/packages/examples/src/createDidWithLocalId.ts index 957e4b087..bd1947119 100644 --- a/packages/examples/src/createDidWithLocalId.ts +++ b/packages/examples/src/createDidWithLocalId.ts @@ -1,13 +1,16 @@ import { ccc } from "@ckb-ccc/ccc"; import { render, signer } from "@ckb-ccc/playground"; -import { secp256k1 } from "@noble/curves/secp256k1"; -import { sha256 } from "@noble/hashes/sha2"; +import { secp256k1 } from "@noble/curves/secp256k1.js"; +import { sha256 } from "@noble/hashes/sha2.js"; // From https://github.com/bluesky-social/atproto/blob/main/packages/crypto function plcSign(key: ccc.BytesLike, msg: ccc.BytesLike): ccc.Bytes { const msgHash = sha256(ccc.bytesFrom(msg)); - const sig = secp256k1.sign(msgHash, ccc.bytesFrom(key), { lowS: true }); - return sig.toBytes("compact"); + return secp256k1.sign(msgHash, ccc.bytesFrom(key), { + lowS: true, + format: "compact", + prehash: false, + }); } // Construct create did tx diff --git a/packages/examples/src/transferFromMultisig.ts b/packages/examples/src/transferFromMultisig.ts new file mode 100644 index 000000000..c6b8808a4 --- /dev/null +++ b/packages/examples/src/transferFromMultisig.ts @@ -0,0 +1,50 @@ +import { ccc } from "@ckb-ccc/ccc"; +import { render, signer } from "@ckb-ccc/playground"; + +// === Prepare multisig signer === + +const signers = [ + "0x2c56a92a03d767542222432e4f2a0584f01e516311f705041d86b1af7573751f", + "0x3bc65932a75f76c5b6a04660e4d0b85c2d9b5114efa78e6e5cf7ad0588ca09c8", + "0xbe06025fbd8c74f65a513a28e62ac56f3227fcb307307a0f2a0ef34d4a66e81f", +].map((key) => new ccc.SignerCkbPrivateKey(signer.client, key)); + +const publicKeys = signers.map((signer) => signer.publicKey); + +const multisigSigners = signers.map( + (signer) => + new ccc.SignerMultisigCkbPrivateKey(signer.client, signer.privateKey, { + publicKeys: publicKeys, + threshold: 2, + mustMatch: 0, + }), +); + +// === Prepare multisig signer === + +const { script: lock } = await signer.getRecommendedAddressObj(); +let tx = ccc.Transaction.from({ + outputs: [{ capacity: ccc.fixedPointFrom(200), lock }], +}); +await tx.completeFeeBy(multisigSigners[0]); +await render(tx); + +for (const multisigSigner of multisigSigners) { + if (await multisigSigner.needMoreSignatures(tx)) { + tx = await multisigSigner.signTransaction(tx); + + const signaturesCount = await multisigSigner.getSignaturesCount(tx); + if (signaturesCount == null) { + console.log( + `Need ${await multisigSigner.getMemberThreshold()} signatures, ${await multisigSigner.getMemberCount()} members in total`, + ); + } else { + console.log( + `${signaturesCount}/${await multisigSigner.getMemberCount()} signers signed, need ${(await multisigSigner.getMemberThreshold()) - signaturesCount} more`, + ); + } + } else { + const txHash = await signer.client.sendTransaction(tx); + console.log(`Transaction ${txHash} sent`); + } +} diff --git a/packages/examples/src/transferFromMultisigAggregateTxs.ts b/packages/examples/src/transferFromMultisigAggregateTxs.ts new file mode 100644 index 000000000..cf67cf94e --- /dev/null +++ b/packages/examples/src/transferFromMultisigAggregateTxs.ts @@ -0,0 +1,44 @@ +import { ccc } from "@ckb-ccc/ccc"; +import { render, signer } from "@ckb-ccc/playground"; + +// === Prepare multisig signer === + +const signers = [ + "0x2c56a92a03d767542222432e4f2a0584f01e516311f705041d86b1af7573751f", + "0x3bc65932a75f76c5b6a04660e4d0b85c2d9b5114efa78e6e5cf7ad0588ca09c8", + "0xbe06025fbd8c74f65a513a28e62ac56f3227fcb307307a0f2a0ef34d4a66e81f", +].map((key) => new ccc.SignerCkbPrivateKey(signer.client, key)); + +const publicKeys = signers.map((signer) => signer.publicKey); + +const multisigSigners = signers.map( + (signer) => + new ccc.SignerMultisigCkbPrivateKey(signer.client, signer.privateKey, { + publicKeys: publicKeys, + threshold: 2, + mustMatch: 0, + }), +); + +// === Prepare multisig signer === + +const { script: lock } = await signer.getRecommendedAddressObj(); +const tx = ccc.Transaction.from({ + outputs: [{ capacity: ccc.fixedPointFrom(200), lock }], +}); +await tx.completeFeeBy(multisigSigners[0]); +await render(tx); + +const collectedTxs = []; +for (const multisigSigner of multisigSigners) { + collectedTxs.push(await multisigSigner.signTransaction(tx.clone())); +} + +const aggregatedTx = + await multisigSigners[0].aggregateTransactions(collectedTxs); +console.log( + `${await multisigSigners[0].getSignaturesCount(aggregatedTx)} signatures aggregated`, +); + +const txHash = await signer.client.sendTransaction(aggregatedTx); +console.log(`Transaction ${txHash} sent`); diff --git a/packages/examples/src/transferToMultisig.ts b/packages/examples/src/transferToMultisig.ts new file mode 100644 index 000000000..a76414df9 --- /dev/null +++ b/packages/examples/src/transferToMultisig.ts @@ -0,0 +1,35 @@ +import { ccc } from "@ckb-ccc/ccc"; +import { render, signer } from "@ckb-ccc/playground"; + +// === Prepare multisig signer === + +const signers = [ + "0x2c56a92a03d767542222432e4f2a0584f01e516311f705041d86b1af7573751f", + "0x3bc65932a75f76c5b6a04660e4d0b85c2d9b5114efa78e6e5cf7ad0588ca09c8", + "0xbe06025fbd8c74f65a513a28e62ac56f3227fcb307307a0f2a0ef34d4a66e81f", +].map((key) => new ccc.SignerCkbPrivateKey(signer.client, key)); + +const publicKeys = signers.map((signer) => signer.publicKey); +const multisigSigner = new ccc.SignerMultisigCkbReadonly(signer.client, { + publicKeys: publicKeys, + threshold: 2, + mustMatch: 0, +}); + +// === Prepare multisig signer === + +// Check the multisig address +const multisigAddress = await multisigSigner.getRecommendedAddressObj(); +console.log("Multisig address:", multisigAddress.toString()); + +// Create a transaction to transfer 1000 CKB to the multisig address +const tx = ccc.Transaction.from({ + outputs: [ + { capacity: ccc.fixedPointFrom(1000), lock: multisigAddress.script }, + ], +}); +await tx.completeFeeBy(signer); +await render(tx); + +const txHash = await signer.sendTransaction(tx); +console.log(`Transaction ${txHash} sent`); diff --git a/packages/playground/next.config.mjs b/packages/playground/next.config.mjs index ea5cd9ba5..ded43ebc7 100644 --- a/packages/playground/next.config.mjs +++ b/packages/playground/next.config.mjs @@ -6,6 +6,10 @@ const nextConfig = { loaders: ["raw-loader"], as: "*.mjs", }, + "*.d.mts": { + loaders: ["raw-loader"], + as: "*.mjs", + }, }, }, }; diff --git a/packages/playground/package.json b/packages/playground/package.json index 1959e46a0..5374dfc50 100644 --- a/packages/playground/package.json +++ b/packages/playground/package.json @@ -16,8 +16,8 @@ "@monaco-editor/react": "^4.7.0", "@nervina-labs/dob-render": "^0.2.5", "@next/third-parties": "^15.5.2", - "@noble/curves": "^1.9.7", - "@noble/hashes": "^1.8.0", + "@noble/curves": "^2.0.1", + "@noble/hashes": "^2.0.1", "@shikijs/monaco": "^3.12.0", "axios": "^1.11.0", "bech32": "^2.0.0", diff --git a/packages/playground/src/app/components/Editor.tsx b/packages/playground/src/app/components/Editor.tsx index 4bd227fb7..d17ce9393 100644 --- a/packages/playground/src/app/components/Editor.tsx +++ b/packages/playground/src/app/components/Editor.tsx @@ -5,12 +5,14 @@ import { editor } from "monaco-editor"; import { useEffect, useRef, useState } from "react"; import { createHighlighter } from "shiki"; +const COMMON_REGEX = /^\.\/(.*\.d\.ts|.*\.d\.mts|package.json)$/; + const EXTRA_SOURCES = [ { files: require.context( "../../../node_modules/@types/react", true, - /^\.\/(.*\.d\.ts|package.json)$/, + COMMON_REGEX, ), name: "@types/react", }, @@ -18,7 +20,7 @@ const EXTRA_SOURCES = [ files: require.context( "../../../../", true, - /^\.\/[^\/]*\/(dist\.commonjs\/.*\.d\.ts|package.json)$/, + /^\.\/[^\/]*\/(dist\/(.*\.d\.ts|.*\.d\.mts)|package.json)$/, ), name: "@ckb-ccc", }, @@ -26,7 +28,7 @@ const EXTRA_SOURCES = [ files: require.context( "../../../node_modules/@nervina-labs/dob-render", true, - /^\.\/(.*\.d\.ts|package.json)$/, + COMMON_REGEX, ), name: "@nervina-labs/dob-render", }, @@ -34,7 +36,7 @@ const EXTRA_SOURCES = [ files: require.context( "../../../node_modules/@noble/hashes", true, - /^\.\/(.*\.d\.ts|package.json)$/, + COMMON_REGEX, ), name: "@noble/hashes", }, @@ -42,7 +44,7 @@ const EXTRA_SOURCES = [ files: require.context( "../../../node_modules/@noble/curves", true, - /^\.\/(.*\.d\.ts|package.json)$/, + COMMON_REGEX, ), name: "@noble/curves", }, @@ -129,7 +131,7 @@ export function Editor({ ...monaco.languages.typescript.typescriptDefaults.getCompilerOptions(), module: monaco.languages.typescript.ModuleKind.ESNext, // eslint-disable-next-line @typescript-eslint/no-explicit-any - moduleResolution: 99 as any, // NodeNext + moduleResolution: 100 as any, // Bundler noImplicitAny: true, strictNullChecks: true, jsx: monaco.languages.typescript.JsxEmit.ReactJSX, diff --git a/packages/playground/src/app/execute/index.tsx b/packages/playground/src/app/execute/index.tsx index cd081999c..c59e61825 100644 --- a/packages/playground/src/app/execute/index.tsx +++ b/packages/playground/src/app/execute/index.tsx @@ -8,8 +8,8 @@ const LIBS_MAP_ = new Map(); const LIBS = await Promise.all( ( [ - ["@noble/curves/secp256k1"], - ["@noble/hashes/sha2"], + ["@noble/curves/secp256k1.js"], + ["@noble/hashes/sha2.js"], ["@ckb-ccc/ccc", "@ckb-ccc/core"], ["@ckb-ccc/ccc/advanced", "@ckb-ccc/core/advanced"], ["@nervina-labs/dob-render"], diff --git a/packages/tests/package.json b/packages/tests/package.json index 3bbd4cf80..93234cb8a 100644 --- a/packages/tests/package.json +++ b/packages/tests/package.json @@ -14,6 +14,7 @@ "test": "node ./tests/cjs.test.cjs && tsx ./tests/esm.test.mts" }, "devDependencies": { + "@ckb-ccc/ccc": "workspace:*", "@eslint/js": "^9.34.0", "eslint": "^9.34.0", "eslint-config-prettier": "^10.1.8", @@ -22,8 +23,7 @@ "prettier-plugin-organize-imports": "^4.2.0", "tsx": "^4.20.5", "typescript": "^5.9.2", - "typescript-eslint": "^8.41.0", - "@ckb-ccc/ccc": "workspace:*" + "typescript-eslint": "^8.41.0" }, "packageManager": "pnpm@10.8.1" } diff --git a/packages/tests/tests/esm.test.mts b/packages/tests/tests/esm.test.mts index 51f5b9bfd..59a4a6ebc 100644 --- a/packages/tests/tests/esm.test.mts +++ b/packages/tests/tests/esm.test.mts @@ -1,11 +1,12 @@ import { ccc } from "@ckb-ccc/ccc"; import assert from "node:assert/strict"; +import { fileURLToPath } from "url"; import path from "path"; assert.ok(ccc, "CCC package should be imported successfully in ESM"); assert.strictEqual( import.meta.resolve("@ckb-ccc/ccc"), - `file://${path.join(import.meta.dirname, "../../ccc/dist/index.js")}`, + `file://${path.join(path.dirname(fileURLToPath(import.meta.url)), "../../ccc/dist/index.js")}`, "CCC package should be imported from dist in ESM", ); console.log("ESM require test passed"); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 33d5adee6..400ac46a5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -234,14 +234,14 @@ importers: specifier: ^1.1.2 version: 1.1.2(typescript@5.9.2) '@noble/ciphers': - specifier: ^0.5.3 - version: 0.5.3 + specifier: ^2.1.1 + version: 2.1.1 '@noble/curves': - specifier: ^1.9.7 - version: 1.9.7 + specifier: ^2.0.1 + version: 2.0.1 '@noble/hashes': - specifier: ^1.8.0 - version: 1.8.0 + specifier: ^2.0.1 + version: 2.0.1 bech32: specifier: ^2.0.0 version: 2.0.0 @@ -267,9 +267,6 @@ importers: '@types/ws': specifier: ^8.18.1 version: 8.18.1 - copyfiles: - specifier: ^2.4.1 - version: 2.4.1 eslint: specifier: ^9.34.0 version: 9.34.0(jiti@2.5.1) @@ -285,9 +282,9 @@ importers: prettier-plugin-organize-imports: specifier: ^4.2.0 version: 4.2.0(prettier@3.6.2)(typescript@5.9.2) - rimraf: - specifier: ^6.0.1 - version: 6.0.1 + tsdown: + specifier: 0.19.0-beta.3 + version: 0.19.0-beta.3(synckit@0.11.11)(typescript@5.9.2) typescript: specifier: ^5.9.2 version: 5.9.2 @@ -533,11 +530,11 @@ importers: specifier: file:src/playground version: playground@file:packages/examples/src/playground '@noble/curves': - specifier: ^1.9.7 - version: 1.9.7 + specifier: ^2.0.1 + version: 2.0.1 '@noble/hashes': - specifier: ^1.8.0 - version: 1.8.0 + specifier: ^2.0.1 + version: 2.0.1 devDependencies: '@eslint/js': specifier: ^9.34.0 @@ -881,11 +878,11 @@ importers: specifier: ^15.5.2 version: 15.5.2(next@16.0.10(@babel/core@7.28.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3) '@noble/curves': - specifier: ^1.9.7 - version: 1.9.7 + specifier: ^2.0.1 + version: 2.0.1 '@noble/hashes': - specifier: ^1.8.0 - version: 1.8.0 + specifier: ^2.0.1 + version: 2.0.1 '@shikijs/monaco': specifier: ^3.12.0 version: 3.12.0 @@ -3696,17 +3693,21 @@ packages: '@noble/ciphers@0.5.3': resolution: {integrity: sha512-B0+6IIHiqEs3BPMT0hcRmHvEj2QHOLu+uwt+tqDDeVd0oyVzh7BPrDcPjRnV1PV/5LaknXJJQvOuRGR0zQJz+w==} + '@noble/ciphers@2.1.1': + resolution: {integrity: sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw==} + engines: {node: '>= 20.19.0'} + '@noble/curves@1.2.0': resolution: {integrity: sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==} - '@noble/curves@1.9.7': - resolution: {integrity: sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==} - engines: {node: ^14.21.3 || >=16} - '@noble/curves@2.0.0': resolution: {integrity: sha512-RiwZZeJnsTnhT+/gg2KvITJZhK5oagQrpZo+yQyd3mv3D5NAG2qEeEHpw7IkXRlpkoD45wl2o4ydHAvY9wyEfw==} engines: {node: '>= 20.19.0'} + '@noble/curves@2.0.1': + resolution: {integrity: sha512-vs1Az2OOTBiP4q0pwjW5aF0xp9n4MxVrmkFBxc6EKZc6ddYx5gaZiAsZoq0uRRXWbi3AT/sBqn05eRPtn1JCPw==} + engines: {node: '>= 20.19.0'} + '@noble/hashes@1.3.2': resolution: {integrity: sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==} engines: {node: '>= 16'} @@ -3719,6 +3720,10 @@ packages: resolution: {integrity: sha512-h8VUBlE8R42+XIDO229cgisD287im3kdY6nbNZJFjc6ZvKIXPYXe6Vc/t+kyjFdMFyt5JpapzTsEg8n63w5/lw==} engines: {node: '>= 20.19.0'} + '@noble/hashes@2.0.1': + resolution: {integrity: sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==} + engines: {node: '>= 20.19.0'} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -11173,7 +11178,7 @@ snapshots: '@babel/helper-annotate-as-pure@7.27.3': dependencies: - '@babel/types': 7.28.2 + '@babel/types': 7.28.5 '@babel/helper-compilation-targets@7.27.2': dependencies: @@ -11219,14 +11224,14 @@ snapshots: '@babel/helper-member-expression-to-functions@7.27.1': dependencies: '@babel/traverse': 7.28.3 - '@babel/types': 7.28.2 + '@babel/types': 7.28.5 transitivePeerDependencies: - supports-color '@babel/helper-module-imports@7.27.1': dependencies: '@babel/traverse': 7.28.3 - '@babel/types': 7.28.2 + '@babel/types': 7.28.5 transitivePeerDependencies: - supports-color @@ -11241,7 +11246,7 @@ snapshots: '@babel/helper-optimise-call-expression@7.27.1': dependencies: - '@babel/types': 7.28.2 + '@babel/types': 7.28.5 '@babel/helper-plugin-utils@7.27.1': {} @@ -11266,7 +11271,7 @@ snapshots: '@babel/helper-skip-transparent-expression-wrappers@7.27.1': dependencies: '@babel/traverse': 7.28.3 - '@babel/types': 7.28.2 + '@babel/types': 7.28.5 transitivePeerDependencies: - supports-color @@ -11282,18 +11287,18 @@ snapshots: dependencies: '@babel/template': 7.27.2 '@babel/traverse': 7.28.3 - '@babel/types': 7.28.2 + '@babel/types': 7.28.5 transitivePeerDependencies: - supports-color '@babel/helpers@7.28.3': dependencies: '@babel/template': 7.27.2 - '@babel/types': 7.28.2 + '@babel/types': 7.28.5 '@babel/highlight@7.25.9': dependencies: - '@babel/helper-validator-identifier': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 chalk: 2.4.2 js-tokens: 4.0.0 picocolors: 1.1.1 @@ -11736,7 +11741,7 @@ snapshots: '@babel/helper-module-imports': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.3) - '@babel/types': 7.28.2 + '@babel/types': 7.28.5 transitivePeerDependencies: - supports-color @@ -11916,7 +11921,7 @@ snapshots: dependencies: '@babel/core': 7.28.3 '@babel/helper-plugin-utils': 7.27.1 - '@babel/types': 7.28.2 + '@babel/types': 7.28.5 esutils: 2.0.3 '@babel/preset-react@7.27.1(@babel/core@7.28.3)': @@ -11953,8 +11958,8 @@ snapshots: '@babel/template@7.27.2': dependencies: '@babel/code-frame': 7.27.1 - '@babel/parser': 7.28.3 - '@babel/types': 7.28.2 + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 '@babel/traverse@7.28.3': dependencies: @@ -14374,24 +14379,28 @@ snapshots: '@noble/ciphers@0.5.3': {} + '@noble/ciphers@2.1.1': {} + '@noble/curves@1.2.0': dependencies: '@noble/hashes': 1.3.2 - '@noble/curves@1.9.7': - dependencies: - '@noble/hashes': 1.8.0 - '@noble/curves@2.0.0': dependencies: '@noble/hashes': 2.0.0 + '@noble/curves@2.0.1': + dependencies: + '@noble/hashes': 2.0.1 + '@noble/hashes@1.3.2': {} '@noble/hashes@1.8.0': {} '@noble/hashes@2.0.0': {} + '@noble/hashes@2.0.1': {} + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -14747,7 +14756,7 @@ snapshots: '@svgr/hast-util-to-babel-ast@8.0.0': dependencies: - '@babel/types': 7.28.2 + '@babel/types': 7.28.5 entities: 4.5.0 '@svgr/plugin-jsx@8.1.0(@svgr/core@8.1.0(typescript@5.9.2))': @@ -14907,24 +14916,24 @@ snapshots: '@types/babel__core@7.20.5': dependencies: - '@babel/parser': 7.28.3 - '@babel/types': 7.28.2 + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 '@types/babel__generator': 7.27.0 '@types/babel__template': 7.4.4 '@types/babel__traverse': 7.28.0 '@types/babel__generator@7.27.0': dependencies: - '@babel/types': 7.28.2 + '@babel/types': 7.28.5 '@types/babel__template@7.4.4': dependencies: - '@babel/parser': 7.28.3 - '@babel/types': 7.28.2 + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 '@types/babel__traverse@7.28.0': dependencies: - '@babel/types': 7.28.2 + '@babel/types': 7.28.5 '@types/blake2b@2.1.3': {} @@ -15954,7 +15963,7 @@ snapshots: babel-plugin-jest-hoist@30.0.1: dependencies: '@babel/template': 7.27.2 - '@babel/types': 7.28.2 + '@babel/types': 7.28.5 '@types/babel__core': 7.20.5 babel-plugin-polyfill-corejs2@0.4.14(@babel/core@7.28.3): @@ -18637,7 +18646,7 @@ snapshots: istanbul-lib-instrument@6.0.3: dependencies: '@babel/core': 7.28.3 - '@babel/parser': 7.28.3 + '@babel/parser': 7.28.5 '@istanbuljs/schema': 0.1.3 istanbul-lib-coverage: 3.2.2 semver: 7.7.3