diff --git a/.changeset/joy-id-sign-psbt-return-psbt.md b/.changeset/joy-id-sign-psbt-return-psbt.md new file mode 100644 index 000000000..cc7b81ea7 --- /dev/null +++ b/.changeset/joy-id-sign-psbt-return-psbt.md @@ -0,0 +1,10 @@ +--- +"@ckb-ccc/joy-id": patch +--- + +`signPsbt` now always returns a PSBT. Previously JoyID returned a raw +transaction when `autoFinalized` was true, and failed on PSBTs with inputs it +couldn't sign. + +If you broadcast the result of `signPsbt` directly, extract the transaction +from the PSBT or use `signAndBroadcastPsbt` instead. diff --git a/.changeset/scure-btc-signer.md b/.changeset/scure-btc-signer.md new file mode 100644 index 000000000..1f7c6f59c --- /dev/null +++ b/.changeset/scure-btc-signer.md @@ -0,0 +1,7 @@ +--- +"@ckb-ccc/joy-id": patch +"@ckb-ccc/xverse": patch +--- + +Use `@scure/btc-signer` instead of `bitcoinjs-lib` for PSBTs. JoyID no longer +calls `initEccLib`, which set global state. diff --git a/packages/examples/package.json b/packages/examples/package.json index 09df2b0b7..780b30574 100644 --- a/packages/examples/package.json +++ b/packages/examples/package.json @@ -34,7 +34,8 @@ "@ckb-ccc/ccc": "workspace:*", "@ckb-ccc/playground": "file:src/playground", "@noble/curves": "^2.2.0", - "@noble/hashes": "^2.2.0" + "@noble/hashes": "^2.2.0", + "@scure/btc-signer": "2.4.1" }, "packageManager": "pnpm@11.8.0" } diff --git a/packages/examples/src/playground/index.d.ts b/packages/examples/src/playground/index.d.ts index 7a7873c29..fefc0e139 100644 --- a/packages/examples/src/playground/index.d.ts +++ b/packages/examples/src/playground/index.d.ts @@ -1,7 +1,5 @@ import { ccc } from "@ckb-ccc/ccc"; -import * as bitcoinLib from "bitcoinjs-lib"; export function render(tx: ccc.Transaction): Promise; export const signer: ccc.Signer; export const client: ccc.Client; -export const bitcoin: typeof bitcoinLib; diff --git a/packages/examples/src/transferBtc.ts b/packages/examples/src/transferBtc.ts index 1af442e66..6488c1549 100644 --- a/packages/examples/src/transferBtc.ts +++ b/packages/examples/src/transferBtc.ts @@ -1,5 +1,6 @@ import { ccc } from "@ckb-ccc/ccc"; -import { bitcoin, signer } from "@ckb-ccc/playground"; +import { signer } from "@ckb-ccc/playground"; +import * as btc from "@scure/btc-signer"; // Supported wallets: Unisat, JoyID, Xverse // Check if the current signer is also a Bitcoin signer @@ -60,46 +61,37 @@ if (!vout || !vout.scriptpubkey) { } // Build PSBT with the selected UTXO as input -const psbt = new bitcoin.Psbt({ - network: isXverse ? bitcoin.networks.testnet : bitcoin.networks.testnet, -}); -const input: { - hash: string; - index: number; - witnessUtxo: { - script: Uint8Array; - value: bigint; - }; - tapInternalKey?: Uint8Array; -} = { - hash: selectedUtxo.txid, +const tx = new btc.Transaction(); +const isTaproot = + vout.scriptpubkey_type === "v1_p2tr" || + vout.scriptpubkey_type === "witness_v1_taproot"; +tx.addInput({ + txid: selectedUtxo.txid, index: selectedUtxo.vout, witnessUtxo: { script: ccc.bytesFrom(vout.scriptpubkey), - value: BigInt(vout.value), + amount: BigInt(vout.value), }, -}; - -// Handle Taproot (P2TR) specific input fields -if ( - vout.scriptpubkey_type === "v1_p2tr" || - vout.scriptpubkey_type === "witness_v1_taproot" -) { - input.tapInternalKey = ccc - .bytesFrom(await signer.getBtcPublicKey()) - .subarray(1); -} - -psbt.addInput(input); + // Taproot inputs need the internal key + ...(isTaproot + ? { + tapInternalKey: ccc + .bytesFrom(await signer.getBtcPublicKey()) + .subarray(1), + } + : {}), +}); // Add a single output back to the same address minus a hardcoded 200 sat fee -psbt.addOutput({ - address: btcAddress, - value: BigInt(vout.value) - BigInt(FEE_SATS), -}); +// Signet uses the testnet address format +tx.addOutputAddress( + btcAddress, + BigInt(vout.value) - BigInt(FEE_SATS), + btc.TEST_NETWORK, +); // Sign and broadcast the transaction -const txId = await signer.signAndBroadcastPsbt(psbt.toHex()); +const txId = await signer.signAndBroadcastPsbt(tx.toPSBT()); console.log( `View transaction: https://mempool.space/${btcTestnetName}/tx/${txId.slice(2)}`, ); diff --git a/packages/joy-id/package.json b/packages/joy-id/package.json index f47719232..89e0ac744 100644 --- a/packages/joy-id/package.json +++ b/packages/joy-id/package.json @@ -40,6 +40,7 @@ }, "devDependencies": { "@eslint/js": "^10.0.1", + "@noble/curves": "^2.2.0", "eslint": "^10.5.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-prettier": "^5.5.6", @@ -56,9 +57,17 @@ }, "dependencies": { "@ckb-ccc/core": "workspace:*", + "@scure/btc-signer": "2.4.1", "@joyid/ckb": "^1.1.4", "@joyid/common": "^0.2.2" }, "packageManager": "pnpm@11.8.0", - "types": "./dist.commonjs/index.d.ts" + "types": "./dist.commonjs/index.d.ts", + "inlinedDependencies": { + "@noble/curves": "2.4.0", + "@noble/hashes": "2.4.0", + "@scure/base": "2.4.0", + "@scure/btc-signer": "2.4.1", + "micro-packed": "0.11.1" + } } diff --git a/packages/joy-id/src/btc/index.test.ts b/packages/joy-id/src/btc/index.test.ts index 96333e1f9..28948d77a 100644 --- a/packages/joy-id/src/btc/index.test.ts +++ b/packages/joy-id/src/btc/index.test.ts @@ -1,5 +1,7 @@ import { ccc } from "@ckb-ccc/core"; import { decodeSearch } from "@joyid/common"; +import { secp256k1 } from "@noble/curves/secp256k1.js"; +import * as btc from "@scure/btc-signer"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { createPopup } from "../common/index.js"; import { @@ -13,7 +15,6 @@ vi.mock("../common/index.js", () => ({ createPopup: vi.fn(), })); -const ADDRESS = "tb1qcqza4qj68la40xfcjg8kdaks7uvjxqhtkh3apm"; const CLIENT = ccc.ClientPublicTestnet.new({ transport: { request: async () => { @@ -22,6 +23,18 @@ const CLIENT = ccc.ClientPublicTestnet.new({ }, }); +function createKey(seed: number) { + const privateKey = new Uint8Array(32).fill(seed); + const publicKey = secp256k1.getPublicKey(privateKey, true); + const { address, script } = btc.p2wpkh(publicKey, btc.TEST_NETWORK); + return { privateKey, publicKey, address, script }; +} + +const JOYID = createKey(1); +const OTHER = createKey(2); +// Key-path finalization only needs a valid x-only key. +const TAPROOT_OUTPUT = ccc.bytesConcat([0x51, 0x20], JOYID.publicKey.slice(1)); + class ConnectionsRepoMemory implements ConnectionsRepo { constructor(public connection?: Connection) {} @@ -46,61 +59,355 @@ function createSigner() { "p2wpkh", undefined, new ConnectionsRepoMemory({ - address: ADDRESS, - publicKey: `0x${"02".repeat(33)}`, + address: JOYID.address, + publicKey: ccc.hexFrom(JOYID.publicKey), keyType: "main_key", }), "btcTestnet", ); } +function createPsbt( + scripts: Uint8Array[], + presign: (tx: btc.Transaction) => void = () => {}, +): ccc.Hex { + const tx = new btc.Transaction({ unknown: "ignore" }); + scripts.forEach((script, i) => { + tx.addInput({ + txid: new Uint8Array(32).fill(i + 1), + index: 0, + witnessUtxo: { script, amount: 10000n }, + }); + }); + tx.addOutputAddress(JOYID.address, 9000n, btc.TEST_NETWORK); + presign(tx); + return ccc.hexFrom(tx.toPSBT()); +} + +// A JoyID signature over a different message. +function staleJoyIdSignature(): [Uint8Array, Uint8Array] { + const signature = secp256k1.sign( + new Uint8Array(32).fill(9), + JOYID.privateKey, + { format: "der" }, + ); + return [JOYID.publicKey, ccc.bytesConcat(signature, [btc.SigHash.ALL])]; +} + +function inputsOf(psbtHex: ccc.Hex) { + const tx = btc.Transaction.fromPSBT(ccc.bytesFrom(psbtHex), { + unknown: "ignore", + }); + return Array.from({ length: tx.inputsLength }, (_, i) => tx.getInput(i)); +} + function requestSentTo(popup: string) { const data = new URL(popup).searchParams.get("_data_"); return decodeSearch(data!) as Record; } +// Signs every JoyID input, replacing any existing JoyID signature. +function mockJoyIdSigning( + sign: (tx: btc.Transaction) => void = (tx) => { + for (let i = 0; i < tx.inputsLength; i++) { + const { witnessUtxo, partialSig } = tx.getInput(i); + if ( + !witnessUtxo || + ccc.hexFrom(witnessUtxo.script) !== ccc.hexFrom(JOYID.script) + ) { + continue; + } + const others = partialSig?.filter( + ([pubkey]) => ccc.hexFrom(pubkey) !== ccc.hexFrom(JOYID.publicKey), + ); + tx.updateInput(i, { partialSig: undefined }); + if (others?.length) { + tx.updateInput(i, { partialSig: others }); + } + tx.signIdx(JOYID.privateKey, i); + } + }, +) { + vi.mocked(createPopup).mockImplementation(async (popup) => { + const tx = btc.Transaction.fromPSBT( + ccc.bytesFrom(requestSentTo(popup).tx as string), + { unknown: "ignore" }, + ); + sign(tx); + return { tx: ccc.hexFrom(tx.toPSBT()).slice(2) }; + }); +} + describe("BitcoinSigner.signPsbt", () => { beforeEach(() => { vi.stubGlobal("location", { href: "https://dapp.test/" }); vi.mocked(createPopup).mockReset(); - vi.mocked(createPopup).mockResolvedValue({ tx: "70736274ff" }); }); - it("should send the default options in JoyID's shape", async () => { - const signed = await createSigner().signPsbt("0x70736274ff"); + it("should ask JoyID not to finalize and finalize locally by default", async () => { + mockJoyIdSigning(); + const psbtHex = createPsbt([JOYID.script]); + + const signed = await createSigner().signPsbt(psbtHex); - expect(signed).toBe("0x70736274ff"); const request = requestSentTo(vi.mocked(createPopup).mock.calls[0][0]); expect(request).toMatchObject({ - tx: "70736274ff", - signerAddress: ADDRESS, - autoFinalized: true, - options: { autoFinalized: true }, + tx: psbtHex.slice(2), + signerAddress: JOYID.address, + autoFinalized: false, + options: { autoFinalized: false }, }); expect(request.options).not.toHaveProperty("toSignInputs"); + + const [input] = inputsOf(signed); + expect(input.finalScriptWitness).toBeDefined(); + expect(input.partialSig).toBeUndefined(); + }); + + it("should return the partially signed PSBT when autoFinalized is false", async () => { + mockJoyIdSigning(); + + const signed = await createSigner().signPsbt(createPsbt([JOYID.script]), { + autoFinalized: false, + }); + + const [input] = inputsOf(signed); + expect(input.finalScriptWitness).toBeUndefined(); + expect(input.partialSig).toHaveLength(1); }); it("should send inputsToSign as toSignInputs", async () => { - await createSigner().signPsbt("0x70736274ff", { + mockJoyIdSigning(); + + await createSigner().signPsbt(createPsbt([JOYID.script, OTHER.script]), { autoFinalized: false, inputsToSign: [ - { index: 0, address: ADDRESS }, + { index: 0, address: JOYID.address }, { index: 1, publicKey: "0xabcd", sighashTypes: [1] }, ], }); - const request = requestSentTo(vi.mocked(createPopup).mock.calls[0][0]); - expect(request).toMatchObject({ + expect( + requestSentTo(vi.mocked(createPopup).mock.calls[0][0]), + ).toMatchObject({ autoFinalized: false, options: { autoFinalized: false, toSignInputs: [ - { index: 0, address: ADDRESS }, + { index: 0, address: JOYID.address }, { index: 1, publicKey: "abcd", sighashTypes: [1] }, ], }, }); }); + + it("should only finalize the inputs JoyID signed", async () => { + mockJoyIdSigning(); + + const signed = await createSigner().signPsbt( + createPsbt([JOYID.script, OTHER.script]), + ); + + const [mine, theirs] = inputsOf(signed); + expect(mine.finalScriptWitness).toBeDefined(); + expect(theirs.finalScriptWitness).toBeUndefined(); + expect(theirs.partialSig).toBeUndefined(); + }); + + it("should finalize without UTXO info on other signers' inputs", async () => { + mockJoyIdSigning(); + const tx = new btc.Transaction(); + tx.addInput({ + txid: new Uint8Array(32).fill(1), + index: 0, + witnessUtxo: { script: JOYID.script, amount: 10000n }, + }); + tx.addInput({ txid: new Uint8Array(32).fill(2), index: 0 }); + tx.addOutputAddress(JOYID.address, 9000n, btc.TEST_NETWORK); + + const signed = await createSigner().signPsbt(ccc.hexFrom(tx.toPSBT())); + + const [mine, theirs] = inputsOf(signed); + expect(mine.finalScriptWitness).toBeDefined(); + expect(theirs.finalScriptWitness).toBeUndefined(); + }); + + it("should finalize P2SH-P2WPKH inputs", async () => { + const nested = btc.p2sh(btc.p2wpkh(JOYID.publicKey), btc.TEST_NETWORK); + mockJoyIdSigning((tx) => tx.signIdx(JOYID.privateKey, 0)); + const tx = new btc.Transaction(); + tx.addInput({ + txid: new Uint8Array(32).fill(1), + index: 0, + witnessUtxo: { script: nested.script, amount: 10000n }, + redeemScript: nested.redeemScript, + }); + tx.addOutputAddress(JOYID.address, 9000n, btc.TEST_NETWORK); + + const signed = await createSigner().signPsbt(ccc.hexFrom(tx.toPSBT())); + + const [input] = inputsOf(signed); + expect(input.finalScriptSig).toBeDefined(); + expect(input.finalScriptWitness).toBeDefined(); + expect(input.redeemScript).toBeUndefined(); + expect(input.partialSig).toBeUndefined(); + }); + + it("should keep PSBT fields it does not know", async () => { + mockJoyIdSigning(); + const psbtHex = createPsbt([JOYID.script], (tx) => + tx.updateInput(0, { + unknown: [ + [{ type: 0xee, key: new Uint8Array([1]) }, new Uint8Array([7])], + ], + }), + ); + + const signed = await createSigner().signPsbt(psbtHex); + + const [input] = inputsOf(signed); + expect(input.finalScriptWitness).toBeDefined(); + expect(input.unknown).toHaveLength(1); + }); + + it("should finalize Taproot key-path inputs", async () => { + mockJoyIdSigning((tx) => + tx.updateInput(0, { tapKeySig: new Uint8Array(64).fill(1) }), + ); + + const signed = await createSigner().signPsbt(createPsbt([TAPROOT_OUTPUT])); + + const [input] = inputsOf(signed); + expect(input.finalScriptWitness).toBeDefined(); + expect(input.tapKeySig).toBeUndefined(); + }); + + it("should finalize an input whose signature JoyID replaced", async () => { + mockJoyIdSigning(); + const psbtHex = createPsbt([JOYID.script], (tx) => + tx.updateInput(0, { partialSig: [staleJoyIdSignature()] }), + ); + + const signed = await createSigner().signPsbt(psbtHex); + + expect(inputsOf(signed)[0].finalScriptWitness).toBeDefined(); + }); + + it("should not finalize inputs signed only by other signers", async () => { + mockJoyIdSigning(); + const psbtHex = createPsbt([JOYID.script, OTHER.script], (tx) => + tx.signIdx(OTHER.privateKey, 1), + ); + + const signed = await createSigner().signPsbt(psbtHex); + + const [mine, theirs] = inputsOf(signed); + expect(mine.finalScriptWitness).toBeDefined(); + expect(theirs.finalScriptWitness).toBeUndefined(); + expect(theirs.partialSig).toHaveLength(1); + }); + + it("should throw when JoyID skips a requested input signed by others", async () => { + mockJoyIdSigning(); + const psbtHex = createPsbt([JOYID.script, OTHER.script], (tx) => + tx.signIdx(OTHER.privateKey, 1), + ); + + await expect( + createSigner().signPsbt(psbtHex, { + inputsToSign: [ + { index: 0, address: JOYID.address }, + { index: 1, address: OTHER.address }, + ], + }), + ).rejects.toThrow( + "JoyID did not add a signature to the requested input #1", + ); + }); + + it("should throw when JoyID skips an unsigned requested input", async () => { + mockJoyIdSigning(); + + await expect( + createSigner().signPsbt(createPsbt([JOYID.script, OTHER.script]), { + inputsToSign: [{ index: 1, address: OTHER.address }], + }), + ).rejects.toThrow( + "JoyID did not add a signature to the requested input #1", + ); + }); + + it("should suggest autoFinalized false when finalizing fails", async () => { + mockJoyIdSigning((tx) => + tx.updateInput(0, { partialSig: [staleJoyIdSignature()] }), + ); + + await expect( + createSigner().signPsbt(createPsbt([TAPROOT_OUTPUT])), + ).rejects.toThrow("Use { autoFinalized: false }"); + }); + + it.each([true, false])( + "should reject a raw transaction (autoFinalized: %s)", + async (autoFinalized) => { + vi.mocked(createPopup).mockResolvedValue({ tx: "02000000000101aabb" }); + + await expect( + createSigner().signPsbt(createPsbt([JOYID.script]), { autoFinalized }), + ).rejects.toThrow("JoyID did not return a PSBT"); + }, + ); + + it.each([true, false])( + "should reject a corrupted PSBT (autoFinalized: %s)", + async (autoFinalized) => { + vi.mocked(createPopup).mockResolvedValue({ tx: "70736274ff0100aabb" }); + + await expect( + createSigner().signPsbt(createPsbt([JOYID.script]), { autoFinalized }), + ).rejects.toThrow("JoyID returned an invalid PSBT"); + }, + ); + + it.each([true, false])( + "should reject a PSBT for a different transaction (autoFinalized: %s)", + async (autoFinalized) => { + vi.mocked(createPopup).mockResolvedValue({ + tx: createPsbt([OTHER.script, OTHER.script]).slice(2), + }); + + await expect( + createSigner().signPsbt(createPsbt([JOYID.script]), { autoFinalized }), + ).rejects.toThrow("different transaction"); + }, + ); + + describe("inputs already signed by JoyID (unsupported)", () => { + const presigned = () => + createPsbt([JOYID.script], (tx) => tx.signIdx(JOYID.privateKey, 0)); + + it("should leave the input unfinalized by default", async () => { + mockJoyIdSigning(); + + const signed = await createSigner().signPsbt(presigned()); + + const [input] = inputsOf(signed); + expect(input.finalScriptWitness).toBeUndefined(); + expect(input.partialSig).toHaveLength(1); + }); + + it("should report the requested input as not signed", async () => { + mockJoyIdSigning(); + + await expect( + createSigner().signPsbt(presigned(), { + inputsToSign: [{ index: 0, address: JOYID.address }], + }), + ).rejects.toThrow( + "JoyID did not add a signature to the requested input #0", + ); + }); + }); }); describe("BitcoinSigner.signAndBroadcastPsbt", () => { @@ -113,7 +420,7 @@ describe("BitcoinSigner.signAndBroadcastPsbt", () => { it("should force autoFinalized on and request broadcasting", async () => { const txid = await createSigner().signAndBroadcastPsbt("0x70736274ff", { autoFinalized: false, - inputsToSign: [{ index: 0, address: ADDRESS }], + inputsToSign: [{ index: 0, address: JOYID.address }], }); expect(txid).toBe(`0x${"ab".repeat(32)}`); @@ -124,7 +431,7 @@ describe("BitcoinSigner.signAndBroadcastPsbt", () => { autoFinalized: true, options: { autoFinalized: true, - toSignInputs: [{ index: 0, address: ADDRESS }], + toSignInputs: [{ index: 0, address: JOYID.address }], }, }); }); diff --git a/packages/joy-id/src/btc/index.ts b/packages/joy-id/src/btc/index.ts index 1705c25bf..f8ba8e838 100644 --- a/packages/joy-id/src/btc/index.ts +++ b/packages/joy-id/src/btc/index.ts @@ -6,6 +6,7 @@ import { ConnectionsRepo, ConnectionsRepoLocalStorage, } from "../connectionsStorage/index.js"; +import { finalizeSignedInputs, parseSignedPsbt } from "./psbt.js"; /** * Converts CCC-level sign PSBT options into the shape expected by JoyID, @@ -208,7 +209,14 @@ export class BitcoinSigner extends ccc.SignerBtc { * Signs a PSBT using JoyID wallet. * * @param psbtHex - The hex string of PSBT to sign. + * @param options - Options for signing the PSBT * @returns A promise that resolves to the signed PSBT as a Hex string. + * + * @remarks + * JoyID returns a raw transaction when asked to finalize, and rejects PSBTs + * with inputs it can't sign. So JoyID is always asked for an unfinalized + * PSBT, and `autoFinalized` is handled here. Inputs that already have a + * JoyID signature are not finalized. */ async signPsbt( psbtHex: ccc.HexLike, @@ -216,16 +224,19 @@ export class BitcoinSigner extends ccc.SignerBtc { ): Promise { const { address } = await this.assertConnection(); const formattedOptions = ccc.SignPsbtOptions.from(options); + const unsignedPsbtHex = ccc.hexFrom(psbtHex); const config = this.getConfig(); - const { tx: signedPsbtHex } = await createPopup( + const { tx } = await createPopup( buildJoyIDURL( { ...config, - tx: ccc.hexFrom(psbtHex).slice(2), - options: toJoyIdSignPsbtOptions(formattedOptions), + tx: unsignedPsbtHex.slice(2), + options: toJoyIdSignPsbtOptions( + new ccc.SignPsbtOptions(false, formattedOptions.inputsToSign), + ), signerAddress: address, - autoFinalized: formattedOptions.autoFinalized, + autoFinalized: false, }, "popup", "/sign-psbt", @@ -233,7 +244,19 @@ export class BitcoinSigner extends ccc.SignerBtc { { ...config, type: DappRequestType.SignPsbt }, ); - return ccc.hexFrom(signedPsbtHex); + const signedPsbtHex = ccc.hexFrom(tx); + const { unsigned, signed } = parseSignedPsbt( + unsignedPsbtHex, + signedPsbtHex, + ); + if (!formattedOptions.autoFinalized) { + return signedPsbtHex; + } + return finalizeSignedInputs( + unsigned, + signed, + formattedOptions.inputsToSign, + ); } /** diff --git a/packages/joy-id/src/btc/psbt.ts b/packages/joy-id/src/btc/psbt.ts new file mode 100644 index 000000000..32f15b2ae --- /dev/null +++ b/packages/joy-id/src/btc/psbt.ts @@ -0,0 +1,155 @@ +import { ccc } from "@ckb-ccc/core"; +import { Transaction, type TxOpts } from "@scure/btc-signer"; + +type PsbtInput = ReturnType; +type PsbtInputUpdate = Parameters[1]; + +// scure drops unknown PSBT fields by default. +const PSBT_OPTIONS: TxOpts = { unknown: "ignore", proprietary: "ignore" }; + +// finalizeIdx needs every input's UTXO to check the fee, so finalize the input +// alone. +function finalizeInput(tx: Transaction, index: number) { + const input = tx.getInput(index); + const scratch = new Transaction(PSBT_OPTIONS); + scratch.addInput(input); + scratch.finalizeIdx(0); + const finalized = scratch.getInput(0); + const cleared = Object.fromEntries( + Object.keys(input) + .filter((key) => !(key in finalized)) + .map((key) => [key, undefined]), + ) as PsbtInputUpdate; + + // Fields a signature commits to can't be cleared while it exists. + tx.updateInput(index, { + partialSig: undefined, + tapKeySig: undefined, + tapScriptSig: undefined, + }); + tx.updateInput(index, { + ...cleared, + finalScriptSig: finalized.finalScriptSig, + finalScriptWitness: finalized.finalScriptWitness, + }); +} + +function inputsOf(tx: Transaction): PsbtInput[] { + return Array.from({ length: tx.inputsLength }, (_, i) => tx.getInput(i)); +} + +function isFinalized(input: PsbtInput) { + return !!(input.finalScriptSig || input.finalScriptWitness); +} + +// Compare by content so a replaced signature counts as new. +function signatureEntries(input: PsbtInput | undefined): Set { + const entries = new Set(); + input?.partialSig?.forEach(([pubkey, signature]) => + entries.add(`partial:${ccc.hexFrom(pubkey)}:${ccc.hexFrom(signature)}`), + ); + if (input?.tapKeySig) { + entries.add(`tapKey:${ccc.hexFrom(input.tapKeySig)}`); + } + input?.tapScriptSig?.forEach(([{ pubKey, leafHash }, signature]) => + entries.add( + `tapScript:${ccc.hexFrom(pubKey)}:${ccc.hexFrom(leafHash)}:${ccc.hexFrom(signature)}`, + ), + ); + return entries; +} + +function hasNewSignature( + before: PsbtInput | undefined, + after: PsbtInput, +): boolean { + const existing = signatureEntries(before); + return [...signatureEntries(after)].some((entry) => !existing.has(entry)); +} + +function finalizeInputs(tx: Transaction, indexes: Set): ccc.Hex { + try { + for (const index of indexes) { + finalizeInput(tx, index); + } + } catch (error) { + throw new Error( + "Failed to finalize the PSBT signed by JoyID. " + + "Use { autoFinalized: false } for partial or multisig signing.", + { cause: error }, + ); + } + + return ccc.hexFrom(tx.toPSBT()); +} + +/** + * Checks that JoyID returned a PSBT of the same transaction. + */ +export function parseSignedPsbt( + unsignedPsbtHex: ccc.Hex, + signedPsbtHex: ccc.Hex, +): { unsigned: Transaction; signed: Transaction } { + if (!signedPsbtHex.startsWith("0x70736274ff")) { + throw new Error("JoyID did not return a PSBT."); + } + + const unsigned = Transaction.fromPSBT( + ccc.bytesFrom(unsignedPsbtHex), + PSBT_OPTIONS, + ); + let signed: Transaction; + try { + signed = Transaction.fromPSBT(ccc.bytesFrom(signedPsbtHex), PSBT_OPTIONS); + } catch (error) { + throw new Error("JoyID returned an invalid PSBT.", { cause: error }); + } + + if (ccc.hexFrom(unsigned.unsignedTx) !== ccc.hexFrom(signed.unsignedTx)) { + throw new Error("JoyID returned a PSBT for a different transaction."); + } + + return { unsigned, signed }; +} + +/** + * Finalizes the inputs JoyID signed. Inputs signed only by others are skipped, + * and every requested input must be signed by JoyID. + * + * Inputs that already had a JoyID signature are not supported, as re-signing + * produces the same bytes. + */ +export function finalizeSignedInputs( + unsigned: Transaction, + signed: Transaction, + inputsToSign: ccc.InputToSign[], +): ccc.Hex { + const before = inputsOf(unsigned); + const after = inputsOf(signed); + const signedIndexes = new Set( + after.flatMap((input, index) => + !isFinalized(input) && hasNewSignature(before[index], input) + ? [index] + : [], + ), + ); + + if (inputsToSign.length === 0) { + return finalizeInputs(signed, signedIndexes); + } + + const requestedIndexes = new Set(); + for (const { index } of inputsToSign) { + const input = after[index]; + if (input && isFinalized(input)) { + continue; + } + if (!signedIndexes.has(index)) { + throw new Error( + `JoyID did not add a signature to the requested input #${index}.`, + ); + } + requestedIndexes.add(index); + } + return finalizeInputs(signed, requestedIndexes); +} diff --git a/packages/joy-id/tsdown.config.mts b/packages/joy-id/tsdown.config.mts index 4951574d0..e80e9e21d 100644 --- a/packages/joy-id/tsdown.config.mts +++ b/packages/joy-id/tsdown.config.mts @@ -15,7 +15,17 @@ const entry = { advancedBarrel: "src/advancedBarrel.ts", } as const; -const bundleDeps: string[] = []; +// ESM-only, so bundled for CommonJS. +const bundleDeps: string[] = [ + "@scure/btc-signer", + "@scure/btc-signer/*", + "@scure/base", + "micro-packed", + "@noble/curves", + "@noble/curves/*", + "@noble/hashes", + "@noble/hashes/*", +]; export default defineConfig( ( diff --git a/packages/playground/package.json b/packages/playground/package.json index f353cec06..b38256af3 100644 --- a/packages/playground/package.json +++ b/packages/playground/package.json @@ -11,7 +11,6 @@ "format": "prettier --write ./src && eslint --fix ./src" }, "dependencies": { - "@bitcoinerlab/secp256k1": "^2.0.0", "@ckb-ccc/ccc": "workspace:*", "@ckb-ccc/connector-react": "workspace:*", "@ckb-ccc/spore": "workspace:*", @@ -20,12 +19,14 @@ "@next/third-parties": "16.3.5", "@noble/curves": "^2.2.0", "@noble/hashes": "^2.2.0", + "@scure/base": "2.4.0", + "@scure/btc-signer": "2.4.1", "@shikijs/monaco": "^4.3.0", "axios": "^1.11.0", "bech32": "^2.0.0", - "bitcoinjs-lib": "^7.0.0", "isomorphic-ws": "^5.0.0", "lucide-react": "^1.21.0", + "micro-packed": "0.11.1", "monaco-editor": "^0.56.0", "next": "16.3.5", "prettier": "^3.9.1", diff --git a/packages/playground/src/app/components/Editor.tsx b/packages/playground/src/app/components/Editor.tsx index ce32a2989..941e8721c 100644 --- a/packages/playground/src/app/components/Editor.tsx +++ b/packages/playground/src/app/components/Editor.tsx @@ -51,6 +51,30 @@ const EXTRA_SOURCES = [ ), name: "@noble/curves", }, + { + files: webpackRequire.context( + "../../../node_modules/@scure/btc-signer", + true, + COMMON_REGEX, + ), + name: "@scure/btc-signer", + }, + { + files: webpackRequire.context( + "../../../node_modules/@scure/base", + true, + COMMON_REGEX, + ), + name: "@scure/base", + }, + { + files: webpackRequire.context( + "../../../node_modules/micro-packed", + true, + COMMON_REGEX, + ), + name: "micro-packed", + }, ]; export function Editor({ @@ -162,7 +186,7 @@ export function Editor({ }); monaco.languages.typescript.typescriptDefaults.addExtraLib( - "import { ccc } from '@ckb-ccc/core'; import * as bitcoin from 'bitcoinjs-lib'; export { bitcoin }; export function render(...msgs: unknown[]): Promise; export const signer: ccc.Signer; export const client: ccc.Client;", + "import { ccc } from '@ckb-ccc/core'; export function render(...msgs: unknown[]): Promise; export const signer: ccc.Signer; export const client: ccc.Client; /** @deprecated Removed. Use `import * as btc from \"@scure/btc-signer\"`. */ export const bitcoin: never;", "file:///node_modules/@ckb-ccc/playground/index.d.ts", ); monaco.languages.typescript.typescriptDefaults.addExtraLib( diff --git a/packages/playground/src/app/execute/index.tsx b/packages/playground/src/app/execute/index.tsx index f20a55ed1..440caeda5 100644 --- a/packages/playground/src/app/execute/index.tsx +++ b/packages/playground/src/app/execute/index.tsx @@ -1,12 +1,5 @@ -import * as ecc from "@bitcoinerlab/secp256k1"; import { ccc } from "@ckb-ccc/connector-react"; -import * as bitcoin from "bitcoinjs-lib"; import * as React from "react"; - -// Initialize the ECC library for bitcoinjs-lib to support Schnorr signatures (Taproot). -// This must be done once globally before any PSBT operations. -bitcoin.initEccLib(ecc); - import ts from "typescript"; import { formatTimestamp } from "../utils"; import { vlqDecode } from "./vlq"; @@ -17,6 +10,7 @@ const LIBS = await Promise.all( [ ["@noble/curves/secp256k1.js"], ["@noble/hashes/sha2.js"], + ["@scure/btc-signer"], ["@ckb-ccc/ccc", "@ckb-ccc/core"], ["@ckb-ccc/ccc/advanced", "@ckb-ccc/core/advanced"], ["@ckb-ccc/spore"], @@ -28,6 +22,18 @@ const LIBS = await Promise.all( }), ).then(() => LIBS_MAP_); +// Old saved scripts may still use `bitcoin` (bitcoinjs-lib). +const removedBitcoin = new Proxy( + {}, + { + get() { + throw new Error( + 'The playground no longer provides "bitcoin" (bitcoinjs-lib). Use `import * as btc from "@scure/btc-signer"` instead.', + ); + }, + }, +); + function findSourcePos( sourceMap: string | undefined, row: number, @@ -115,7 +121,7 @@ export async function execute( }, signer, client: signer.client, - bitcoin, + bitcoin: removedBitcoin, }; } diff --git a/packages/xverse/package.json b/packages/xverse/package.json index cb4f02ea9..0a5cc15b5 100644 --- a/packages/xverse/package.json +++ b/packages/xverse/package.json @@ -57,9 +57,16 @@ }, "dependencies": { "@ckb-ccc/core": "workspace:*", - "bitcoinjs-lib": "^7.0.1", + "@scure/btc-signer": "2.4.1", "valibot": "^1.4.1" }, "packageManager": "pnpm@11.8.0", - "types": "./dist.commonjs/index.d.ts" + "types": "./dist.commonjs/index.d.ts", + "inlinedDependencies": { + "@noble/curves": "2.4.0", + "@noble/hashes": "2.4.0", + "@scure/base": "2.4.0", + "@scure/btc-signer": "2.4.1", + "micro-packed": "0.11.1" + } } diff --git a/packages/xverse/src/signer.test.ts b/packages/xverse/src/signer.test.ts index 73a1cce0b..9e2d84676 100644 --- a/packages/xverse/src/signer.test.ts +++ b/packages/xverse/src/signer.test.ts @@ -1,6 +1,6 @@ import { ccc } from "@ckb-ccc/core"; import { secp256k1 } from "@noble/curves/secp256k1.js"; -import { networks, payments, Psbt } from "bitcoinjs-lib"; +import * as btc from "@scure/btc-signer"; import { describe, expect, it, vi } from "vitest"; import { AddressPurpose, @@ -10,7 +10,6 @@ import { } from "./advancedBarrel.js"; import { Signer } from "./signer.js"; -const NETWORK = networks.testnet; const CLIENT = ccc.ClientPublicTestnet.new({ transport: { request: async () => { @@ -22,47 +21,38 @@ const CLIENT = ccc.ClientPublicTestnet.new({ function createKey(seed: number) { const privateKey = new Uint8Array(32).fill(seed); const publicKey = secp256k1.getPublicKey(privateKey, true); - const { address, output } = payments.p2wpkh({ - pubkey: publicKey, - network: NETWORK, - }); - return { - address: address!, - publicKey, - output: output!, - signer: { - publicKey, - sign: (hash: Uint8Array) => secp256k1.sign(hash, privateKey), - }, - }; + const { address, script } = btc.p2wpkh(publicKey, btc.TEST_NETWORK); + return { privateKey, publicKey, address, script }; } const WALLET = createKey(1); const OTHER = createKey(2); -function createPsbt(...inputs: { output: Uint8Array }[]): ccc.Hex { - const psbt = new Psbt({ network: NETWORK }); - inputs.forEach(({ output }, i) => { - psbt.addInput({ - hash: new Uint8Array(32).fill(i + 1), +function createPsbt(...inputs: { script: Uint8Array }[]): ccc.Hex { + const tx = new btc.Transaction({ unknown: "ignore" }); + inputs.forEach(({ script }, i) => { + tx.addInput({ + txid: new Uint8Array(32).fill(i + 1), index: 0, - witnessUtxo: { script: output, value: 10000n }, + witnessUtxo: { script, amount: 10000n }, }); }); - psbt.addOutput({ address: WALLET.address, value: 9000n }); - return ccc.hexFrom(psbt.toBuffer()); + tx.addOutputAddress(WALLET.address, 9000n, btc.TEST_NETWORK); + return ccc.hexFrom(tx.toPSBT()); } function createSigner({ sign = true } = {}) { const signPsbt = vi.fn((params: SignPsbtParams) => { - const psbt = Psbt.fromBase64(params.psbt, { network: NETWORK }); + const tx = btc.Transaction.fromPSBT(ccc.bytesFrom(params.psbt, "base64"), { + unknown: "ignore", + }); if (sign) { Object.entries(params.signInputs).forEach(([address, indexes]) => { expect(address).toBe(WALLET.address); - indexes.forEach((index) => psbt.signInput(index, WALLET.signer)); + indexes.forEach((index) => tx.signIdx(WALLET.privateKey, index)); }); } - return { psbt: psbt.toBase64() }; + return { psbt: ccc.bytesTo(tx.toPSBT(), "base64") }; }); const provider = { @@ -94,8 +84,11 @@ function createSigner({ sign = true } = {}) { return { signer: new Signer(CLIENT, provider), signPsbt }; } -function parseSigned(signed: ccc.Hex) { - return Psbt.fromBuffer(ccc.bytesFrom(signed), { network: NETWORK }); +function inputsOf(signed: ccc.Hex) { + const tx = btc.Transaction.fromPSBT(ccc.bytesFrom(signed), { + unknown: "ignore", + }); + return Array.from({ length: tx.inputsLength }, (_, i) => tx.getInput(i)); } describe("Signer.signPsbt", () => { @@ -110,7 +103,7 @@ describe("Signer.signPsbt", () => { broadcast: false, }), ); - const [input] = parseSigned(signed).data.inputs; + const [input] = inputsOf(signed); expect(input.finalScriptWitness).toBeDefined(); expect(input.partialSig).toBeUndefined(); }); @@ -122,7 +115,7 @@ describe("Signer.signPsbt", () => { autoFinalized: false, }); - const [input] = parseSigned(signed).data.inputs; + const [input] = inputsOf(signed); expect(input.finalScriptWitness).toBeUndefined(); expect(input.partialSig).toHaveLength(1); }); @@ -137,7 +130,7 @@ describe("Signer.signPsbt", () => { expect(signPsbt).toHaveBeenCalledWith( expect.objectContaining({ signInputs: { [WALLET.address]: [0] } }), ); - const [mine, theirs] = parseSigned(signed).data.inputs; + const [mine, theirs] = inputsOf(signed); expect(mine.finalScriptWitness).toBeDefined(); expect(theirs.finalScriptWitness).toBeUndefined(); expect(theirs.partialSig).toBeUndefined(); @@ -151,6 +144,40 @@ describe("Signer.signPsbt", () => { ); }); + it("should finalize without UTXO info on other signers' inputs", async () => { + const { signer } = createSigner(); + const tx = btc.Transaction.fromPSBT(ccc.bytesFrom(createPsbt(WALLET)), { + unknown: "ignore", + }); + tx.addInput({ txid: new Uint8Array(32).fill(9), index: 0 }); + + const signed = await signer.signPsbt(ccc.hexFrom(tx.toPSBT()), { + inputsToSign: [{ index: 0, address: WALLET.address }], + }); + + const [mine, theirs] = inputsOf(signed); + expect(mine.finalScriptWitness).toBeDefined(); + expect(theirs.finalScriptWitness).toBeUndefined(); + }); + + it("should keep PSBT fields it does not know", async () => { + const { signer } = createSigner(); + const tx = btc.Transaction.fromPSBT(ccc.bytesFrom(createPsbt(WALLET)), { + unknown: "ignore", + }); + tx.updateInput(0, { + unknown: [ + [{ type: 0xee, key: new Uint8Array([1]) }, new Uint8Array([7])], + ], + }); + + const signed = await signer.signPsbt(ccc.hexFrom(tx.toPSBT())); + + const [input] = inputsOf(signed); + expect(input.finalScriptWitness).toBeDefined(); + expect(input.unknown).toHaveLength(1); + }); + it("should require an address in inputsToSign", async () => { const { signer } = createSigner(); diff --git a/packages/xverse/src/signer.ts b/packages/xverse/src/signer.ts index c92d3eb11..72848da79 100644 --- a/packages/xverse/src/signer.ts +++ b/packages/xverse/src/signer.ts @@ -1,5 +1,5 @@ import { ccc } from "@ckb-ccc/core"; -import { Psbt } from "bitcoinjs-lib"; +import { Transaction, type TxOpts } from "@scure/btc-signer"; import * as v from "valibot"; import { Address, @@ -15,6 +15,38 @@ import { rpcSuccessResponseMessageSchema, } from "./advancedBarrel.js"; +// scure drops unknown PSBT fields by default. +const PSBT_OPTIONS: TxOpts = { unknown: "ignore", proprietary: "ignore" }; + +type PsbtInputUpdate = Parameters[1]; + +// finalizeIdx needs every input's UTXO to check the fee, so finalize the input +// alone. +function finalizeInput(tx: Transaction, index: number) { + const input = tx.getInput(index); + const scratch = new Transaction(PSBT_OPTIONS); + scratch.addInput(input); + scratch.finalizeIdx(0); + const finalized = scratch.getInput(0); + const cleared = Object.fromEntries( + Object.keys(input) + .filter((key) => !(key in finalized)) + .map((key) => [key, undefined]), + ) as PsbtInputUpdate; + + // Fields a signature commits to can't be cleared while it exists. + tx.updateInput(index, { + partialSig: undefined, + tapKeySig: undefined, + tapScriptSig: undefined, + }); + tx.updateInput(index, { + ...cleared, + finalScriptSig: finalized.finalScriptSig, + finalScriptWitness: finalized.finalScriptWitness, + }); +} + async function checkResponse( response: Promise>, ): Promise> { @@ -206,8 +238,9 @@ export class Signer extends ccc.SignerBtc { try { // Collect all unsigned inputs - const psbt = Psbt.fromHex(psbtHex.slice(2)); - psbt.data.inputs.forEach((input, index) => { + const tx = Transaction.fromPSBT(ccc.bytesFrom(psbtHex), PSBT_OPTIONS); + for (let index = 0; index < tx.inputsLength; index++) { + const input = tx.getInput(index); const isSigned = input.finalScriptSig || input.finalScriptWitness || @@ -218,7 +251,7 @@ export class Signer extends ccc.SignerBtc { if (!isSigned) { inputsToSign.push(ccc.InputToSign.from({ index, address })); } - }); + } // If no unsigned inputs found, the PSBT is already fully signed // Let the wallet handle this case (likely a no-op or error) @@ -278,18 +311,18 @@ export class Signer extends ccc.SignerBtc { * leaving inputs owned by other signers untouched. */ private finalizeSignedInputs( - psbt: Psbt, + tx: Transaction, signInputs: Record, ): void { const indexes = new Set(Object.values(signInputs).flat()); try { for (const index of indexes) { - const input = psbt.data.inputs[index]; + const input = index < tx.inputsLength ? tx.getInput(index) : undefined; if (input?.finalScriptSig || input?.finalScriptWitness) { continue; } - psbt.finalizeInput(index); + finalizeInput(tx, index); } } catch (error) { throw new Error( @@ -348,9 +381,12 @@ export class Signer extends ccc.SignerBtc { return ccc.hexFrom(ccc.bytesFrom(signedPsbtBase64, "base64")); } - const signedPsbt = Psbt.fromBase64(signedPsbtBase64); + const signedPsbt = Transaction.fromPSBT( + ccc.bytesFrom(signedPsbtBase64, "base64"), + PSBT_OPTIONS, + ); this.finalizeSignedInputs(signedPsbt, signInputs); - return ccc.hexFrom(signedPsbt.toBuffer()); + return ccc.hexFrom(signedPsbt.toPSBT()); } /** diff --git a/packages/xverse/tsdown.config.mts b/packages/xverse/tsdown.config.mts index 4951574d0..e80e9e21d 100644 --- a/packages/xverse/tsdown.config.mts +++ b/packages/xverse/tsdown.config.mts @@ -15,7 +15,17 @@ const entry = { advancedBarrel: "src/advancedBarrel.ts", } as const; -const bundleDeps: string[] = []; +// ESM-only, so bundled for CommonJS. +const bundleDeps: string[] = [ + "@scure/btc-signer", + "@scure/btc-signer/*", + "@scure/base", + "micro-packed", + "@noble/curves", + "@noble/curves/*", + "@noble/hashes", + "@noble/hashes/*", +]; export default defineConfig( ( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8ca71dc18..19c8a4981 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -632,6 +632,9 @@ importers: '@noble/hashes': specifier: ^2.2.0 version: 2.4.0 + '@scure/btc-signer': + specifier: 2.4.1 + version: 2.4.1 devDependencies: '@eslint/js': specifier: ^10.0.1 @@ -790,10 +793,16 @@ importers: '@joyid/common': specifier: ^0.2.2 version: 0.2.2(@typescript/typescript6@6.0.2) + '@scure/btc-signer': + specifier: 2.4.1 + version: 2.4.1 devDependencies: '@eslint/js': specifier: ^10.0.1 version: 10.0.1(eslint@10.10.0(jiti@2.7.0)) + '@noble/curves': + specifier: ^2.2.0 + version: 2.4.0 '@typescript/native': specifier: npm:typescript@^7.0.2 version: typescript@7.0.2 @@ -1020,9 +1029,6 @@ importers: packages/playground: dependencies: - '@bitcoinerlab/secp256k1': - specifier: ^2.0.0 - version: 2.0.0 '@ckb-ccc/ccc': specifier: workspace:* version: link:../ccc @@ -1047,6 +1053,12 @@ importers: '@noble/hashes': specifier: ^2.2.0 version: 2.4.0 + '@scure/base': + specifier: 2.4.0 + version: 2.4.0 + '@scure/btc-signer': + specifier: 2.4.1 + version: 2.4.1 '@shikijs/monaco': specifier: ^4.3.0 version: 4.4.3 @@ -1056,15 +1068,15 @@ importers: bech32: specifier: ^2.0.0 version: 2.0.0 - bitcoinjs-lib: - specifier: ^7.0.0 - version: 7.0.2(@typescript/typescript6@6.0.2) isomorphic-ws: specifier: ^5.0.0 version: 5.0.0(ws@8.21.3) lucide-react: specifier: ^1.21.0 version: 1.47.0(react@19.3.0) + micro-packed: + specifier: 0.11.1 + version: 0.11.1 monaco-editor: specifier: ^0.56.0 version: 0.56.0 @@ -1528,9 +1540,9 @@ importers: '@ckb-ccc/core': specifier: workspace:* version: link:../core - bitcoinjs-lib: - specifier: ^7.0.1 - version: 7.0.2(@typescript/typescript6@6.0.2) + '@scure/btc-signer': + specifier: 2.4.1 + version: 2.4.1 valibot: specifier: ^1.4.1 version: 1.5.0(@typescript/typescript6@6.0.2) @@ -1843,10 +1855,6 @@ packages: resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} engines: {node: '>=18'} - '@bitcoinerlab/secp256k1@2.0.0': - resolution: {integrity: sha512-l8wO4Hx7fovtcDVU5xYEa7yfP9DokC8QxYlJoaP1zi2/iX+c6NNSvMzq+oCZ8ikqvrXTuhSQ6iP1IRx08kWZgw==} - engines: {node: '>=20.19.0'} - '@borewit/text-codec@0.2.2': resolution: {integrity: sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==} @@ -4271,6 +4279,9 @@ packages: '@scure/bip39@2.4.0': resolution: {integrity: sha512-82dxFbZUYboyOf0AXiydsQrFQ5Q4h9mX+O2UkE91ROYmsc0BKMGZLwDmy96Jpa2+vrtoxomjUhy1RPIgH/r2nA==} + '@scure/btc-signer@2.4.1': + resolution: {integrity: sha512-ch6h66o9nPhEoe/KciMMUJeH2MBPB8UAJI1if35Fxl6885lKTsc9YoCRqTW0/RjI0Vv9GSg1tmKtHHODyAw81g==} + '@shikijs/core@4.4.3': resolution: {integrity: sha512-QCR4q2ZO/ILJEuwiBMel4wdcTDb1JGwfjKTxPDF6x8ixOaluPrVqIn06C99AcRPhmYlBR56d/Fb+GN58GzExpg==} engines: {node: '>=20'} @@ -5686,14 +5697,6 @@ packages: big.js@5.2.2: resolution: {integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==} - bip174@3.0.1: - resolution: {integrity: sha512-avOnOoLOwdjw1MebWKh3bHqWOH558Q9VpqmRKUv2S63vjw0pnX7Y+yRihxIibgP55twhc7+YfC0IAH43IF5FqQ==} - engines: {node: '>=18.0.0'} - - bitcoinjs-lib@7.0.2: - resolution: {integrity: sha512-ugLk9SR+2gvhpNA7kqQL1eVDKyBNo/rbr+df72nrZj4OOdZvQRHDVtyogPGXAU2MLQimLL7EbUdz95QGh6Th5Q==} - engines: {node: '>=18.0.0'} - blake2b-wasm@2.4.0: resolution: {integrity: sha512-S1kwmW2ZhZFFFOghcx73+ZajEfKBqhP82JMssxtLVMxlaPea1p9uoLiUZ5WYyHn0KddwbLc+0vh4wR0KBNoT5w==} @@ -8419,6 +8422,10 @@ packages: engines: {node: ^22.13.0 || ^24.3.0 || >= 26.0.0} hasBin: true + micro-packed@0.11.1: + resolution: {integrity: sha512-agQGBnBCw8OoKQZMUxcaQPaOowdfxyEmBsq4CNAXAXCyB8KxlRG0lTl5GADJIsMEDaFb0trhXBHKYsjTWa5kuw==} + engines: {node: '>= 20.19.0'} + micromark-core-commonmark@2.0.3: resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} @@ -10241,14 +10248,6 @@ packages: resolution: {integrity: sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==} engines: {node: '>=18'} - uint8array-tools@0.0.10: - resolution: {integrity: sha512-sSbrZqlr04w4p8KKpHw0ME/R4JzQaadum+BWhMlk2M7LrDyXWTfJOco3d1jNMdQNxgk5DDm88R3IWYH0nR+tzQ==} - engines: {node: '>=14.0.0'} - - uint8array-tools@0.0.9: - resolution: {integrity: sha512-9vqDWmoSXOoi+K14zNaf6LBV51Q8MayF0/IiQs3GlygIKUYtog603e6virExkjjFosfJUBI4LhbQK1iq8IG11A==} - engines: {node: '>=14.0.0'} - uint8arraylist@2.4.9: resolution: {integrity: sha512-KxWjyEFzchzik3aoQlK66oaoxIReoMo5bQRm1fcjBUZvE8xv/tyR3CTKhjh6K/faV8VaF6hd5pjr45CzbwuwkA==} @@ -10405,9 +10404,6 @@ packages: resolution: {integrity: sha512-TQ5pAGhd5whStmqWvYF4OjQROlmv9SMFVt37qoCBdqRffuuklWYQlCNnEs2ZaIBD1kZRNnikiZOS1eqgkar0iw==} engines: {node: '>= 0.10'} - varuint-bitcoin@2.0.1: - resolution: {integrity: sha512-TTl5qSPJ3FzjuTHo0Ex9WUE3jTxnUsQHR38vIDhHApoVstzJK4gE20WqmTWOT5oo7ilvOSNejXt4CJrfDcqm5A==} - vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} @@ -11073,10 +11069,6 @@ snapshots: '@bcoe/v8-coverage@1.0.2': {} - '@bitcoinerlab/secp256k1@2.0.0': - dependencies: - '@noble/curves': 2.4.0 - '@borewit/text-codec@0.2.2': {} '@braintree/sanitize-url@7.1.2': {} @@ -13643,6 +13635,13 @@ snapshots: dependencies: '@noble/hashes': 2.4.0 + '@scure/btc-signer@2.4.1': + dependencies: + '@noble/curves': 2.4.0 + '@noble/hashes': 2.4.0 + '@scure/base': 2.4.0 + micro-packed: 0.11.1 + '@shikijs/core@4.4.3': dependencies: '@shikijs/primitive': 4.4.3 @@ -15025,23 +15024,6 @@ snapshots: big.js@5.2.2: {} - bip174@3.0.1: - dependencies: - uint8array-tools: 0.0.10 - varuint-bitcoin: 2.0.1 - - bitcoinjs-lib@7.0.2(@typescript/typescript6@6.0.2): - dependencies: - '@noble/hashes': 1.8.0 - bech32: 2.0.0 - bip174: 3.0.1 - bs58check: 4.0.0 - uint8array-tools: 0.0.9 - valibot: 1.5.0(@typescript/typescript6@6.0.2) - varuint-bitcoin: 2.0.1 - transitivePeerDependencies: - - typescript - blake2b-wasm@2.4.0: dependencies: b4a: 1.6.7 @@ -18638,6 +18620,10 @@ snapshots: - supports-color - utf-8-validate + micro-packed@0.11.1: + dependencies: + '@scure/base': 2.4.0 + micromark-core-commonmark@2.0.3: dependencies: decode-named-character-reference: 1.3.0 @@ -20779,10 +20765,6 @@ snapshots: uint8array-extras@1.5.0: {} - uint8array-tools@0.0.10: {} - - uint8array-tools@0.0.9: {} - uint8arraylist@2.4.9: dependencies: uint8arrays: 5.1.1 @@ -20966,10 +20948,6 @@ snapshots: validator@13.15.35: {} - varuint-bitcoin@2.0.1: - dependencies: - uint8array-tools: 0.0.10 - vary@1.1.2: {} verkit@0.4.0: {}