@kukks/bitcoin-descriptors 3.0.2 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,23 @@
1
+ import { HDKey } from '@scure/bip32';
2
+ import type { ECPairInterface, ECPairAPI, BIP32Interface, BIP32API } from './types.js';
3
+ import { type Network } from './networks.js';
4
+ /**
5
+ * Create an ECPairInterface from a private key and/or public key.
6
+ * Exported for advanced use cases where consumers need to construct
7
+ * key pairs directly.
8
+ */
9
+ export declare function createECPair(privKey: Uint8Array | undefined, pubKeyInput: Uint8Array | undefined, compressed: boolean, network: Network): ECPairInterface;
10
+ /**
11
+ * Built-in ECPairAPI implementation using @noble/curves secp256k1.
12
+ */
13
+ export declare const nobleECPair: ECPairAPI;
14
+ /**
15
+ * Wrap a @scure/bip32 HDKey in a BIP32Interface.
16
+ * Exported for advanced use cases where consumers need to wrap
17
+ * HDKey instances directly.
18
+ */
19
+ export declare function wrapHDKey(hdkey: HDKey, network: Network): BIP32Interface;
20
+ /**
21
+ * Built-in BIP32API implementation using @scure/bip32.
22
+ */
23
+ export declare const scureBIP32: BIP32API;
@@ -0,0 +1,321 @@
1
+ // Copyright (c) 2025 Jose-Luis Landabaso - https://bitcoinerlab.com
2
+ // Distributed under the MIT software license
3
+ // Built-in adapters for ECPairAPI and BIP32API using
4
+ // @noble/curves, @scure/bip32, @noble/hashes, and @scure/base.
5
+ // These conform to the interfaces defined in ./types.ts.
6
+ import { secp256k1 } from '@noble/curves/secp256k1.js';
7
+ import { HDKey } from '@scure/bip32';
8
+ import { sha256 } from '@noble/hashes/sha2.js';
9
+ import { base58check, hex } from '@scure/base';
10
+ import { concatBytes } from '@scure/btc-signer/utils.js';
11
+ import { networks } from './networks.js';
12
+ // ---------------------------------------------------------------------------
13
+ // Helpers
14
+ // ---------------------------------------------------------------------------
15
+ const bs58check = base58check(sha256);
16
+ const DEFAULT_NETWORK = networks.bitcoin;
17
+ /** Convert a bigint to a 32-byte big-endian Uint8Array. */
18
+ function bigintTo32Bytes(n) {
19
+ const hexStr = n.toString(16).padStart(64, '0');
20
+ return hex.decode(hexStr);
21
+ }
22
+ /** Write a 32-bit unsigned integer in big-endian format into buf at offset. */
23
+ function writeUInt32BE(buf, value, offset) {
24
+ buf[offset] = (value >>> 24) & 0xff;
25
+ buf[offset + 1] = (value >>> 16) & 0xff;
26
+ buf[offset + 2] = (value >>> 8) & 0xff;
27
+ buf[offset + 3] = value & 0xff;
28
+ }
29
+ // ---------------------------------------------------------------------------
30
+ // WIF encode / decode
31
+ // ---------------------------------------------------------------------------
32
+ function encodeWIF(privateKey, compressed, network) {
33
+ const prefix = Uint8Array.from([network.wif]);
34
+ const payload = compressed
35
+ ? concatBytes(prefix, privateKey, Uint8Array.from([0x01]))
36
+ : concatBytes(prefix, privateKey);
37
+ return bs58check.encode(payload);
38
+ }
39
+ function decodeWIF(wif, network) {
40
+ const decoded = bs58check.decode(wif);
41
+ const version = decoded[0];
42
+ // Determine matching network(s)
43
+ const candidateNetworks = network
44
+ ? Array.isArray(network)
45
+ ? network
46
+ : [network]
47
+ : [networks.bitcoin, networks.testnet, networks.regtest];
48
+ const matchedNetwork = candidateNetworks.find(n => n.wif === version);
49
+ if (!matchedNetwork) {
50
+ throw new Error(`Invalid network version`);
51
+ }
52
+ // Determine if compressed
53
+ if (decoded.length === 34 && decoded[33] === 0x01) {
54
+ return {
55
+ privateKey: decoded.subarray(1, 33),
56
+ compressed: true,
57
+ network: matchedNetwork
58
+ };
59
+ }
60
+ else if (decoded.length === 33) {
61
+ return {
62
+ privateKey: decoded.subarray(1, 33),
63
+ compressed: false,
64
+ network: matchedNetwork
65
+ };
66
+ }
67
+ else {
68
+ throw new Error('Invalid WIF payload length');
69
+ }
70
+ }
71
+ // ---------------------------------------------------------------------------
72
+ // ECPair implementation
73
+ // ---------------------------------------------------------------------------
74
+ /**
75
+ * Create an ECPairInterface from a private key and/or public key.
76
+ * Exported for advanced use cases where consumers need to construct
77
+ * key pairs directly.
78
+ */
79
+ export function createECPair(privKey, pubKeyInput, compressed, network) {
80
+ let pubKey;
81
+ if (privKey) {
82
+ pubKey = secp256k1.getPublicKey(privKey, compressed);
83
+ }
84
+ else if (pubKeyInput) {
85
+ // Normalise to the requested compression
86
+ const point = secp256k1.Point.fromHex(hex.encode(pubKeyInput));
87
+ pubKey = point.toBytes(compressed);
88
+ }
89
+ else {
90
+ throw new Error('Either privateKey or publicKey must be provided');
91
+ }
92
+ // Build the object conditionally so that `privateKey` is only present
93
+ // when defined (satisfies exactOptionalPropertyTypes).
94
+ const base = {
95
+ publicKey: pubKey,
96
+ compressed,
97
+ network,
98
+ sign(hash) {
99
+ if (!privKey)
100
+ throw new Error('Missing private key');
101
+ return secp256k1.sign(hash, privKey);
102
+ },
103
+ verify(hash, signature) {
104
+ return secp256k1.verify(signature, hash, pubKey);
105
+ },
106
+ toWIF() {
107
+ if (!privKey)
108
+ throw new Error('Missing private key');
109
+ return encodeWIF(privKey, compressed, network);
110
+ },
111
+ tweak(t) {
112
+ const tweakBigint = BigInt('0x' + hex.encode(t));
113
+ const n = secp256k1.Point.CURVE().n;
114
+ if (privKey) {
115
+ const privBigint = BigInt('0x' + hex.encode(privKey));
116
+ const newPrivBigint = (privBigint + tweakBigint) % n;
117
+ if (newPrivBigint === 0n) {
118
+ throw new Error('Tweaked private key is zero');
119
+ }
120
+ const newPrivKey = bigintTo32Bytes(newPrivBigint);
121
+ return createECPair(newPrivKey, undefined, compressed, network);
122
+ }
123
+ else {
124
+ // Public-key-only tweak: P' = P + t*G
125
+ const G = secp256k1.Point.BASE;
126
+ const tweakPoint = G.multiply(tweakBigint);
127
+ const pubPoint = secp256k1.Point.fromHex(hex.encode(pubKey));
128
+ const tweakedPoint = pubPoint.add(tweakPoint);
129
+ return createECPair(undefined, tweakedPoint.toBytes(compressed), compressed, network);
130
+ }
131
+ }
132
+ };
133
+ if (privKey) {
134
+ return Object.assign(base, { privateKey: privKey });
135
+ }
136
+ return base;
137
+ }
138
+ /**
139
+ * Built-in ECPairAPI implementation using @noble/curves secp256k1.
140
+ */
141
+ export const nobleECPair = {
142
+ fromPublicKey(publicKey, options) {
143
+ const compressed = options?.compressed !== false;
144
+ const network = options?.network ?? DEFAULT_NETWORK;
145
+ return createECPair(undefined, publicKey, compressed, network);
146
+ },
147
+ fromPrivateKey(privateKey, options) {
148
+ const compressed = options?.compressed !== false;
149
+ const network = options?.network ?? DEFAULT_NETWORK;
150
+ return createECPair(privateKey, undefined, compressed, network);
151
+ },
152
+ fromWIF(wif, network) {
153
+ const { privateKey, compressed, network: net } = decodeWIF(wif, network);
154
+ return createECPair(privateKey, undefined, compressed, net);
155
+ },
156
+ makeRandom(options) {
157
+ const privKey = secp256k1.utils.randomSecretKey();
158
+ const compressed = options?.compressed !== false;
159
+ const network = options?.network ?? DEFAULT_NETWORK;
160
+ return createECPair(privKey, undefined, compressed, network);
161
+ },
162
+ isPoint(p) {
163
+ try {
164
+ secp256k1.Point.fromHex(hex.encode(p));
165
+ return true;
166
+ }
167
+ catch {
168
+ return false;
169
+ }
170
+ }
171
+ };
172
+ // ---------------------------------------------------------------------------
173
+ // BIP32 implementation
174
+ // ---------------------------------------------------------------------------
175
+ /** Map a Network to the versions object expected by @scure/bip32's HDKey. */
176
+ function networkToVersions(network) {
177
+ return { public: network.bip32.public, private: network.bip32.private };
178
+ }
179
+ /**
180
+ * Wrap a @scure/bip32 HDKey in a BIP32Interface.
181
+ * Exported for advanced use cases where consumers need to wrap
182
+ * HDKey instances directly.
183
+ */
184
+ export function wrapHDKey(hdkey, network) {
185
+ const versions = networkToVersions(network);
186
+ // Build the base object without privateKey, then conditionally add it.
187
+ // This satisfies exactOptionalPropertyTypes.
188
+ const base = {
189
+ get publicKey() {
190
+ if (!hdkey.publicKey)
191
+ throw new Error('Missing public key');
192
+ return hdkey.publicKey;
193
+ },
194
+ get chainCode() {
195
+ if (!hdkey.chainCode)
196
+ throw new Error('Missing chain code');
197
+ return hdkey.chainCode;
198
+ },
199
+ get fingerprint() {
200
+ // HDKey.fingerprint is a number (4-byte big-endian unsigned int)
201
+ const buf = new Uint8Array(4);
202
+ writeUInt32BE(buf, hdkey.fingerprint >>> 0, 0);
203
+ return buf;
204
+ },
205
+ get depth() {
206
+ return hdkey.depth;
207
+ },
208
+ get index() {
209
+ return hdkey.index;
210
+ },
211
+ get parentFingerprint() {
212
+ return hdkey.parentFingerprint;
213
+ },
214
+ network,
215
+ derivePath(path) {
216
+ // @scure/bip32 requires paths to start with "m" or "M"
217
+ // Old bip32 package accepted relative paths like "0'/1'/0'"
218
+ const normalizedPath = path.startsWith('m') || path.startsWith('M') ? path : `m/${path}`;
219
+ return wrapHDKey(hdkey.derive(normalizedPath), network);
220
+ },
221
+ derive(index) {
222
+ return wrapHDKey(hdkey.deriveChild(index), network);
223
+ },
224
+ deriveHardened(index) {
225
+ return wrapHDKey(hdkey.deriveChild(index + 0x80000000), network);
226
+ },
227
+ neutered() {
228
+ // Create a public-only HDKey via the public extended key
229
+ const xpub = hdkey.publicExtendedKey;
230
+ const neuteredKey = HDKey.fromExtendedKey(xpub, versions);
231
+ return wrapHDKey(neuteredKey, network);
232
+ },
233
+ toBase58() {
234
+ if (hdkey.privateKey) {
235
+ return hdkey.privateExtendedKey;
236
+ }
237
+ return hdkey.publicExtendedKey;
238
+ },
239
+ sign(hash) {
240
+ if (!hdkey.privateKey)
241
+ throw new Error('Missing private key');
242
+ return hdkey.sign(hash);
243
+ },
244
+ verify(hash, signature) {
245
+ return hdkey.verify(hash, signature);
246
+ },
247
+ isNeutered() {
248
+ return !hdkey.privateKey;
249
+ },
250
+ toWIF() {
251
+ if (!hdkey.privateKey) {
252
+ throw new Error('Missing private key');
253
+ }
254
+ return encodeWIF(hdkey.privateKey, true, network);
255
+ }
256
+ };
257
+ if (hdkey.privateKey) {
258
+ return Object.defineProperty(base, 'privateKey', {
259
+ get() {
260
+ return hdkey.privateKey;
261
+ },
262
+ enumerable: true,
263
+ configurable: true
264
+ });
265
+ }
266
+ return base;
267
+ }
268
+ /**
269
+ * Built-in BIP32API implementation using @scure/bip32.
270
+ */
271
+ export const scureBIP32 = {
272
+ fromBase58(base58, network) {
273
+ const net = network ?? DEFAULT_NETWORK;
274
+ const versions = networkToVersions(net);
275
+ const hdkey = HDKey.fromExtendedKey(base58, versions);
276
+ return wrapHDKey(hdkey, net);
277
+ },
278
+ fromPublicKey(publicKey, chainCode, network) {
279
+ const net = network ?? DEFAULT_NETWORK;
280
+ const versions = networkToVersions(net);
281
+ // Manually serialise the public extended key in base58check format.
282
+ //
283
+ // xpub format (78 bytes):
284
+ // version (4) + depth (1) + parentFingerprint (4) + index (4) +
285
+ // chainCode (32) + publicKey (33)
286
+ const buf = new Uint8Array(78);
287
+ writeUInt32BE(buf, versions.public, 0); // version
288
+ buf[4] = 0; // depth
289
+ writeUInt32BE(buf, 0, 5); // parent fingerprint
290
+ writeUInt32BE(buf, 0, 9); // index
291
+ buf.set(chainCode, 13);
292
+ buf.set(publicKey, 45);
293
+ const xpub = bs58check.encode(buf);
294
+ const hdkey = HDKey.fromExtendedKey(xpub, versions);
295
+ return wrapHDKey(hdkey, net);
296
+ },
297
+ fromPrivateKey(privateKey, chainCode, network) {
298
+ const net = network ?? DEFAULT_NETWORK;
299
+ const versions = networkToVersions(net);
300
+ // xprv format (78 bytes):
301
+ // version (4) + depth (1) + parentFingerprint (4) + index (4) +
302
+ // chainCode (32) + 0x00 (1) + privateKey (32)
303
+ const buf = new Uint8Array(78);
304
+ writeUInt32BE(buf, versions.private, 0); // version
305
+ buf[4] = 0; // depth
306
+ writeUInt32BE(buf, 0, 5); // parent fingerprint
307
+ writeUInt32BE(buf, 0, 9); // index
308
+ buf.set(chainCode, 13);
309
+ buf[45] = 0x00; // padding byte before private key
310
+ buf.set(privateKey, 46);
311
+ const xprv = bs58check.encode(buf);
312
+ const hdkey = HDKey.fromExtendedKey(xprv, versions);
313
+ return wrapHDKey(hdkey, net);
314
+ },
315
+ fromSeed(seed, network) {
316
+ const net = network ?? DEFAULT_NETWORK;
317
+ const versions = networkToVersions(net);
318
+ const hdkey = HDKey.fromMasterSeed(seed, versions);
319
+ return wrapHDKey(hdkey, net);
320
+ }
321
+ };
@@ -16,11 +16,12 @@ import type { PsbtLike } from './psbt.js';
16
16
  * These are compatible interfaces for managing BIP32 keys and
17
17
  * public/private key pairs respectively.
18
18
  *
19
- * @param {Object} params - An object with `ECPair` and `BIP32` factories.
19
+ * @param {Object} params - An object with optional `ECPair` and `BIP32` factories.
20
+ * When omitted, built-in adapters using @noble/curves and @scure/bip32 are used.
20
21
  */
21
- export declare function DescriptorsFactory({ ECPair, BIP32 }: {
22
- ECPair: ECPairAPI;
23
- BIP32: BIP32API;
22
+ export declare function DescriptorsFactory({ ECPair, BIP32 }?: {
23
+ ECPair?: ECPairAPI;
24
+ BIP32?: BIP32API;
24
25
  }): {
25
26
  Output: {
26
27
  new ({ descriptor, index, checksumRequired, allowMiniscriptInP2SH, network, preimages, signersPubKeys }: {
@@ -30,6 +30,7 @@ import { varintEncodingLength, decompileScript } from './scriptUtils.js';
30
30
  import { hash160, compareBytes, equalBytes, concatBytes } from '@scure/btc-signer/utils.js';
31
31
  import { hex } from '@scure/base';
32
32
  import { sha256 } from '@noble/hashes/sha2.js';
33
+ import { nobleECPair, scureBIP32 } from './adapters.js';
33
34
  import { computeFinalScripts, updatePsbt } from './psbt.js';
34
35
  import { DescriptorChecksum } from './checksum.js';
35
36
  import { parseKeyExpression as globalParseKeyExpression } from './keyExpressions.js';
@@ -178,9 +179,10 @@ function parseSortedMulti(inner) {
178
179
  * These are compatible interfaces for managing BIP32 keys and
179
180
  * public/private key pairs respectively.
180
181
  *
181
- * @param {Object} params - An object with `ECPair` and `BIP32` factories.
182
+ * @param {Object} params - An object with optional `ECPair` and `BIP32` factories.
183
+ * When omitted, built-in adapters using @noble/curves and @scure/bip32 are used.
182
184
  */
183
- export function DescriptorsFactory({ ECPair, BIP32 }) {
185
+ export function DescriptorsFactory({ ECPair = nobleECPair, BIP32 = scureBIP32 } = {}) {
184
186
  var _Output_instances, _Output_payment, _Output_preimages, _Output_signersPubKeys, _Output_miniscript, _Output_witnessScript, _Output_redeemScript, _Output_isSegwit, _Output_isTaproot, _Output_expandedExpression, _Output_expandedMiniscript, _Output_expansionMap, _Output_network, _Output_getTimeConstraints, _Output_assertPsbtInput;
185
187
  /**
186
188
  * Takes a string key expression (xpub, xprv, pubkey or wif) and parses it
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export type { KeyInfo, Expansion } from './types.js';
2
+ export type { ECPairAPI, ECPairInterface, BIP32API, BIP32Interface } from './types.js';
2
3
  export type { OutputInstance } from './descriptors.js';
3
4
  export { DescriptorsFactory, OutputConstructor } from './descriptors.js';
4
5
  export { DescriptorChecksum as checksum } from './checksum.js';
@@ -10,3 +11,91 @@ export { scriptExpressions };
10
11
  export type { PsbtLike } from './psbt.js';
11
12
  export { networks } from './networks.js';
12
13
  export type { Network } from './networks.js';
14
+ export { nobleECPair, scureBIP32 } from './adapters.js';
15
+ export declare const defaultFactory: {
16
+ Output: {
17
+ new ({ descriptor, index, checksumRequired, allowMiniscriptInP2SH, network, preimages, signersPubKeys }: {
18
+ descriptor: string;
19
+ index?: number;
20
+ checksumRequired?: boolean;
21
+ allowMiniscriptInP2SH?: boolean;
22
+ network?: import("./networks.js").Network;
23
+ preimages?: import("./types.js").Preimage[];
24
+ signersPubKeys?: Uint8Array[];
25
+ }): {
26
+ readonly "__#private@#payment": import("@scure/btc-signer/payment.js").P2Ret;
27
+ readonly "__#private@#preimages": import("./types.js").Preimage[];
28
+ readonly "__#private@#signersPubKeys": Uint8Array[];
29
+ readonly "__#private@#miniscript"?: string;
30
+ readonly "__#private@#witnessScript"?: Uint8Array;
31
+ readonly "__#private@#redeemScript"?: Uint8Array;
32
+ readonly "__#private@#isSegwit"?: boolean;
33
+ readonly "__#private@#isTaproot"?: boolean;
34
+ readonly "__#private@#expandedExpression"?: string;
35
+ readonly "__#private@#expandedMiniscript"?: string;
36
+ readonly "__#private@#expansionMap"?: import("./types.js").ExpansionMap;
37
+ readonly "__#private@#network": import("./networks.js").Network;
38
+ "__#private@#getTimeConstraints"(): import("./types.js").TimeConstraints | undefined;
39
+ getPayment(): import("@scure/btc-signer/payment.js").P2Ret;
40
+ getAddress(): string;
41
+ getScriptPubKey(): Uint8Array;
42
+ getScriptSatisfaction(signatures: import("./types.js").PartialSig[] | "DANGEROUSLY_USE_FAKE_SIGNATURES"): Uint8Array;
43
+ getSequence(): number | undefined;
44
+ getLockTime(): number | undefined;
45
+ getWitnessScript(): Uint8Array | undefined;
46
+ getRedeemScript(): Uint8Array | undefined;
47
+ getNetwork(): import("./networks.js").Network;
48
+ isSegwit(): boolean | undefined;
49
+ isTaproot(): boolean | undefined;
50
+ guessOutput(): {
51
+ isPKH: boolean;
52
+ isWPKH: boolean;
53
+ isSH: boolean;
54
+ isWSH: boolean;
55
+ isTR: boolean;
56
+ };
57
+ inputWeight(isSegwitTx: boolean, signatures: import("./types.js").PartialSig[] | "DANGEROUSLY_USE_FAKE_SIGNATURES"): number;
58
+ outputWeight(): number;
59
+ updatePsbtAsInput({ psbt, txHex, txId, value, vout, rbf }: {
60
+ psbt: import("./psbt.js").PsbtLike;
61
+ txHex?: string;
62
+ txId?: string;
63
+ value?: number;
64
+ vout: number;
65
+ rbf?: boolean;
66
+ }): ({ psbt, validate }: {
67
+ psbt: import("./psbt.js").PsbtLike;
68
+ validate?: boolean | undefined;
69
+ }) => void;
70
+ updatePsbtAsOutput({ psbt, value }: {
71
+ psbt: import("./psbt.js").PsbtLike;
72
+ value: number | bigint;
73
+ }): void;
74
+ "__#private@#assertPsbtInput"({ psbt, index }: {
75
+ psbt: import("./psbt.js").PsbtLike;
76
+ index: number;
77
+ }): void;
78
+ finalizePsbtInput({ index, psbt, validate }: {
79
+ index: number;
80
+ psbt: import("./psbt.js").PsbtLike;
81
+ validate?: boolean | undefined;
82
+ }): void;
83
+ expand(): {
84
+ expansionMap?: import("./types.js").ExpansionMap;
85
+ expandedMiniscript?: string;
86
+ miniscript?: string;
87
+ expandedExpression?: string;
88
+ };
89
+ };
90
+ };
91
+ parseKeyExpression: import("./types.js").ParseKeyExpression;
92
+ expand: ({ descriptor, index, checksumRequired, network, allowMiniscriptInP2SH }: {
93
+ descriptor: string;
94
+ index?: number;
95
+ checksumRequired?: boolean;
96
+ network?: import("./networks.js").Network;
97
+ allowMiniscriptInP2SH?: boolean;
98
+ }) => import("./types.js").Expansion;
99
+ ECPair: import("./types.js").ECPairAPI;
100
+ BIP32: import("./types.js").BIP32API;
101
+ };
package/dist/index.js CHANGED
@@ -8,3 +8,8 @@ export { keyExpressionBIP32 } from './keyExpressions.js';
8
8
  import * as scriptExpressions from './scriptExpressions.js';
9
9
  export { scriptExpressions };
10
10
  export { networks } from './networks.js';
11
+ // Built-in adapters using @noble/curves and @scure/bip32
12
+ export { nobleECPair, scureBIP32 } from './adapters.js';
13
+ // Pre-built factory using built-in adapters (zero-config convenience)
14
+ import { DescriptorsFactory } from './descriptors.js';
15
+ export const defaultFactory = DescriptorsFactory();
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@kukks/bitcoin-descriptors",
3
3
  "description": "This library parses and creates Bitcoin Miniscript Descriptors and generates Partially Signed Bitcoin Transactions (PSBTs). It provides PSBT finalizers and signers for single-signature, BIP32 and Hardware Wallets.",
4
4
  "homepage": "https://github.com/Kukks/descriptors",
5
- "version": "3.0.2",
5
+ "version": "3.1.0",
6
6
  "author": "Jose-Luis Landabaso",
7
7
  "license": "MIT",
8
8
  "repository": {