Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changeset/joy-id-sign-psbt-return-psbt.md
Original file line number Diff line number Diff line change
@@ -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.
7 changes: 7 additions & 0 deletions .changeset/scure-btc-signer.md
Original file line number Diff line number Diff line change
@@ -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.
12 changes: 12 additions & 0 deletions .changeset/secure-wallet-psbt-responses.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
"@ckb-ccc/xverse": patch
"@ckb-ccc/joy-id": patch
---

Bind signed Bitcoin PSBT responses to the original transaction and prevouts, check actual signature sighash modes and caller policies before finalization, and reject wallet-added final scripts. These checks also apply when automatic finalization is disabled.

Preserve partial/multiparty signing, unrelated bare inputs, and optional Taproot tree metadata while retaining commitment checks. Verify Taproot key-path signatures against the actual output key, rejecting incorrect empty-tree signatures. Use stock scure through a shared private compatibility, validation, and finalization boundary bundled into ESM and CommonJS builds.

Recheck the effective PSBTv2 transaction after metadata restoration, reject removed signature entries, and preserve finalized witness inputs with an explicit empty scriptSig.

Check single-key ECDSA public-key/script binding before automatic finalization, including wrapped payment scripts.
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,16 @@
],
"scripts": {
"test": "pnpm run -C packages/tests test && vitest",
"test:ci": "pnpm run -C packages/tests test && vitest run",
"test:ci": "pnpm run -C packages/tests test && vitest run && pnpm test:psbt:artifacts",
"test:cov": "vitest run --coverage",
"build:prepare": "pnpm -r --filter !./packages/tests --filter !./packages/app --filter !./packages/faucet --filter !./packages/examples --filter !./packages/playground --filter !./packages/docs --filter !. install",
"build": "pnpm -r --filter !./packages/tests --filter !./packages/app --filter !./packages/faucet --filter !./packages/examples --filter !./packages/playground --filter !./packages/docs run build",
"build:all": "pnpm -r run build",
"lint": "pnpm -r run lint",
"format": "pnpm -r run format",
"sync:config": "pnpm -r --filter !./packages/app --filter !./packages/faucet --filter !./packages/playground --filter !./packages/docs -c exec \"cp ../../config/* . && cp ../../config/.* .\"",
"docs": "typedoc"
"docs": "typedoc",
"test:psbt:artifacts": "node scripts/test-psbt-artifacts.mjs"
},
"devDependencies": {
"@changesets/changelog-github": "^1.0.0",
Expand Down
37 changes: 37 additions & 0 deletions packages/btc-psbt/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Private wallet PSBT helpers

This source-only workspace package shares PSBT response validation and per-input
finalization between Xverse and JoyID. It is a development dependency bundled by
both adapters' tsdown configurations, with no independent build or publication.
Core does not depend on it. Wallet communication, input selection, and which inputs
to finalize remain adapter-specific.

`WalletPsbt` owns its scure transaction and input bytes. It exposes copied reads,
guarded serialization, and per-input finalization; no generic mutation or raw
transaction accessor is exposed. Finalization stages updates privately and checks
all inputs before and after mutation. `parseAndValidateSignedPsbt` enforces wallet
response binding, signature encoding, and caller sighash policy. Finalization also
checks encodings/declarations, verifies Taproot key signatures in the full
transaction at the original index, and binds ECDSA keys to single-key scripts.

Stock scure 2.4.1 is used without dependency patches. Parsing first tries its strict
public API. A fallback delegates structural and commitment validation to public
`normalizeInput`, `getPrevOut`, and `OutScript` APIs, with exactly two exceptions:

- Recognized bare P2PK/multisig inputs may omit wrappers; any redeem/witness script
or Taproot commitment metadata on them is rejected.
- A Taproot internal key without a root or leaves describes an unknown tree.
Its encoding is checked first, then only this key is omitted from a temporary
validation projection. The original metadata is retained. Explicit roots,
leaves, wrapper hashes, and canonical witness programs remain checked by scure.

Every fallback input is checked, including unrelated inputs, and every
serialization repeats these checks. The compatibility suite records acceptance
contracts from the former parser patch and tests malformed metadata, alias
isolation, atomic finalization, and the unrelated-input bypass of raw unchecked
transactions. No upstream private-key signing or finalizer implementation is copied.

Run the common suite with `pnpm --filter @ckb-ccc/btc-psbt test run`, and both
adapter suites with `pnpm --filter @ckb-ccc/xverse --filter @ckb-ccc/joy-id test run`.
After building the adapters, `pnpm test:psbt:artifacts` exercises their published
ESM/CommonJS entry points and checks that this private package is fully bundled.
63 changes: 63 additions & 0 deletions packages/btc-psbt/eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// @ts-check

import eslint from "@eslint/js";
import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended";
import tseslint from "typescript-eslint";

import { dirname } from "path";
import { fileURLToPath } from "url";

export default [
...tseslint.config({
files: ["**/*.ts"],
extends: [
eslint.configs.recommended,
...tseslint.configs.recommendedTypeChecked,
],
rules: {
"@typescript-eslint/no-unused-vars": [
"error",
{
args: "all",
argsIgnorePattern: "^_",
caughtErrors: "all",
caughtErrorsIgnorePattern: "^_",
destructuredArrayIgnorePattern: "^_",
varsIgnorePattern: "^_",
ignoreRestSiblings: true,
},
],
"@typescript-eslint/unbound-method": ["error", { ignoreStatic: true }],
"@typescript-eslint/no-unsafe-member-access": "off",
"@typescript-eslint/require-await": "off",
"@typescript-eslint/only-throw-error": [
"error",
{
allowThrowingAny: true,
allowThrowingUnknown: true,
allowRethrowing: true,
},
],
"@typescript-eslint/prefer-promise-reject-errors": [
"error",
{
allowThrowingAny: true,
allowThrowingUnknown: true,
},
],
"no-empty": "off",
"prefer-const": [
"error",
{ ignoreReadBeforeAssign: true, destructuring: "all" },
],
},
languageOptions: {
parserOptions: {
project: true,
tsconfigRootDir: dirname(fileURLToPath(import.meta.url)),
},
},
}),
eslintPluginPrettierRecommended,
{ rules: { curly: ["error", "all"] } },
];
32 changes: 32 additions & 0 deletions packages/btc-psbt/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"name": "@ckb-ccc/btc-psbt",
"version": "0.0.0",
"description": "Private PSBT validation and finalization shared by wallet adapters",
"private": true,
"license": "MIT",
"sideEffects": false,
"exports": {
".": "./src/index.ts"
},
"scripts": {
"lint": "tsc --noEmit && eslint ./src",
"format": "prettier --write . && eslint --fix ./src",
"test": "vitest"
},
"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",
"prettier": "^3.8.4",
"prettier-plugin-organize-imports": "^4.3.0",
"@typescript/native": "npm:typescript@^7.0.2",
"typescript": "npm:@typescript/typescript6@^6.0.2",
"typescript-eslint": "^8.61.1",
"vitest": "^4.1.9",
"@ckb-ccc/core": "workspace:*",
"@scure/btc-signer": "2.4.1"
},
"packageManager": "pnpm@11.8.0"
}
11 changes: 11 additions & 0 deletions packages/btc-psbt/prettier.config.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/**
* @see https://prettier.io/docs/configuration
* @type {import("prettier").Config}
*/
const config = {
singleQuote: false,
trailingComma: "all",
plugins: [require.resolve("prettier-plugin-organize-imports")],
};

module.exports = config;
Loading
Loading