@kukks/bitcoin-descriptors 3.0.2
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.
- package/README.md +214 -0
- package/dist/checksum.d.ts +6 -0
- package/dist/checksum.js +54 -0
- package/dist/descriptors.d.ts +113 -0
- package/dist/descriptors.js +1033 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +10 -0
- package/dist/keyExpressions.d.ts +59 -0
- package/dist/keyExpressions.js +197 -0
- package/dist/miniscript.d.ts +55 -0
- package/dist/miniscript.js +163 -0
- package/dist/networks.d.ts +23 -0
- package/dist/networks.js +37 -0
- package/dist/psbt.d.ts +38 -0
- package/dist/psbt.js +200 -0
- package/dist/re.d.ts +31 -0
- package/dist/re.js +75 -0
- package/dist/scriptExpressions.d.ts +62 -0
- package/dist/scriptExpressions.js +47 -0
- package/dist/scriptUtils.d.ts +19 -0
- package/dist/scriptUtils.js +124 -0
- package/dist/signers.d.ts +62 -0
- package/dist/signers.js +129 -0
- package/dist/types.d.ts +263 -0
- package/dist/types.js +3 -0
- package/package.json +68 -0
package/dist/psbt.js
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
// Copyright (c) 2025 Jose-Luis Landabaso - https://bitcoinerlab.com
|
|
2
|
+
// Distributed under the MIT software license
|
|
3
|
+
import * as btc from '@scure/btc-signer';
|
|
4
|
+
import { bip32Path } from '@scure/btc-signer';
|
|
5
|
+
import { RawTx, RawOldTx } from '@scure/btc-signer/script.js';
|
|
6
|
+
import { sha256 } from '@noble/hashes/sha2.js';
|
|
7
|
+
import { hex } from '@scure/base';
|
|
8
|
+
import { equalBytes } from '@scure/btc-signer/utils.js';
|
|
9
|
+
import { decompileScript, readUInt32BE } from './scriptUtils.js';
|
|
10
|
+
/**
|
|
11
|
+
* Computes final scripts for miniscript-based inputs.
|
|
12
|
+
*
|
|
13
|
+
* @param scriptSatisfaction - The script satisfaction (input script data)
|
|
14
|
+
* @param lockingScript - The "meaningful" locking script (witnessScript or redeemScript)
|
|
15
|
+
* @param isSegwit - Whether this is a segwit input
|
|
16
|
+
* @param isP2SH - Whether this is a P2SH input
|
|
17
|
+
* @param network - The network
|
|
18
|
+
* @returns Object with finalScriptSig and finalScriptWitness (as Uint8Array[])
|
|
19
|
+
*/
|
|
20
|
+
export function computeFinalScripts(scriptSatisfaction, lockingScript, isSegwit, isP2SH, redeemScript) {
|
|
21
|
+
let finalScriptWitness;
|
|
22
|
+
let finalScriptSig;
|
|
23
|
+
// Helper: decompile scriptSatisfaction into witness items
|
|
24
|
+
function satisfactionToWitness() {
|
|
25
|
+
const chunks = decompileScript(scriptSatisfaction);
|
|
26
|
+
if (!chunks)
|
|
27
|
+
throw new Error('Could not decompile script satisfaction');
|
|
28
|
+
return chunks.map(chunk => typeof chunk === 'number'
|
|
29
|
+
? new Uint8Array(0) // OP_0 or opcodes become empty push
|
|
30
|
+
: chunk);
|
|
31
|
+
}
|
|
32
|
+
//p2wsh: witness = [satisfaction chunks..., witnessScript]
|
|
33
|
+
if (isSegwit && !isP2SH) {
|
|
34
|
+
const witness = satisfactionToWitness();
|
|
35
|
+
witness.push(lockingScript);
|
|
36
|
+
finalScriptWitness = witness;
|
|
37
|
+
}
|
|
38
|
+
//p2sh-p2wsh: witness = [satisfaction chunks..., witnessScript], scriptSig = push of redeemScript
|
|
39
|
+
else if (isSegwit && isP2SH) {
|
|
40
|
+
const witness = satisfactionToWitness();
|
|
41
|
+
witness.push(lockingScript);
|
|
42
|
+
finalScriptWitness = witness;
|
|
43
|
+
// finalScriptSig is a serialized script that pushes the redeemScript
|
|
44
|
+
const rs = redeemScript ?? lockingScript;
|
|
45
|
+
finalScriptSig = btc.Script.encode([rs]);
|
|
46
|
+
}
|
|
47
|
+
//p2sh: scriptSig = satisfaction + push of redeemScript
|
|
48
|
+
else {
|
|
49
|
+
// Build scriptSig: scriptSatisfaction bytes + push of lockingScript (redeemScript)
|
|
50
|
+
const chunks = decompileScript(scriptSatisfaction);
|
|
51
|
+
if (!chunks)
|
|
52
|
+
throw new Error('Could not decompile script satisfaction');
|
|
53
|
+
const scriptItems = chunks.map(chunk => typeof chunk === 'number' ? chunk : chunk);
|
|
54
|
+
scriptItems.push(lockingScript);
|
|
55
|
+
finalScriptSig = btc.Script.encode(scriptItems);
|
|
56
|
+
}
|
|
57
|
+
return { finalScriptSig, finalScriptWitness };
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Important: Read comments on descriptor.updatePsbt regarding not passing txHex
|
|
61
|
+
*/
|
|
62
|
+
export function updatePsbt({ psbt, vout, txHex, txId, value, sequence, locktime, keysInfo, scriptPubKey, isSegwit, tapInternalKey, witnessScript, redeemScript, rbf }) {
|
|
63
|
+
//Some data-sanity checks:
|
|
64
|
+
if (sequence !== undefined && rbf && sequence > 0xfffffffd)
|
|
65
|
+
throw new Error(`Error: incompatible sequence and rbf settings`);
|
|
66
|
+
if (!isSegwit && txHex === undefined)
|
|
67
|
+
throw new Error(`Error: txHex is mandatory for Non-Segwit inputs`);
|
|
68
|
+
if (isSegwit &&
|
|
69
|
+
txHex === undefined &&
|
|
70
|
+
(txId === undefined || value === undefined))
|
|
71
|
+
throw new Error(`Error: pass txHex or txId+value for Segwit inputs`);
|
|
72
|
+
if (txHex !== undefined) {
|
|
73
|
+
const rawTxBytes = hex.decode(txHex);
|
|
74
|
+
// Use RawTx.decode instead of Transaction.fromRaw to avoid script
|
|
75
|
+
// validation that rejects bare P2PK and other non-wrapped output types.
|
|
76
|
+
const parsed = RawTx.decode(rawTxBytes);
|
|
77
|
+
const out = parsed.outputs[vout];
|
|
78
|
+
if (!out)
|
|
79
|
+
throw new Error(`Error: tx ${txHex} does not have vout ${vout}`);
|
|
80
|
+
const outputScript = out.script;
|
|
81
|
+
if (!outputScript)
|
|
82
|
+
throw new Error(`Error: could not extract outputScript for txHex ${txHex} and vout ${vout}`);
|
|
83
|
+
if (!equalBytes(outputScript, scriptPubKey))
|
|
84
|
+
throw new Error(`Error: txHex ${txHex} for vout ${vout} does not correspond to scriptPubKey ${hex.encode(scriptPubKey)}`);
|
|
85
|
+
// Compute txid: double-SHA256 of non-witness serialization, reversed
|
|
86
|
+
const nonWitnessSerialization = RawOldTx.encode(parsed);
|
|
87
|
+
const txidHash = sha256(sha256(nonWitnessSerialization));
|
|
88
|
+
const computedTxId = hex.encode(txidHash.slice().reverse());
|
|
89
|
+
if (txId !== undefined) {
|
|
90
|
+
if (computedTxId !== txId)
|
|
91
|
+
throw new Error(`Error: txId for ${txHex} and vout ${vout} does not correspond to ${txId}`);
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
txId = computedTxId;
|
|
95
|
+
}
|
|
96
|
+
if (value !== undefined) {
|
|
97
|
+
if (BigInt(out.amount) !== BigInt(value))
|
|
98
|
+
throw new Error(`Error: value for ${txHex} and vout ${vout} does not correspond to ${value}`);
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
value = BigInt(out.amount);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (txId === undefined || !value)
|
|
105
|
+
throw new Error(`Error: txHex+vout required. Alternatively, but ONLY for Segwit inputs, txId+value can also be passed.`);
|
|
106
|
+
if (locktime) {
|
|
107
|
+
const currentLocktime = psbt.lockTime;
|
|
108
|
+
if (currentLocktime &&
|
|
109
|
+
currentLocktime !== locktime &&
|
|
110
|
+
currentLocktime !== 0)
|
|
111
|
+
throw new Error(`Error: transaction locktime was already set with a different value: ${locktime} != ${currentLocktime}`);
|
|
112
|
+
// nLockTime only works if at least one of the transaction inputs has an
|
|
113
|
+
// nSequence value that is below 0xffffffff. Let's make sure that at least
|
|
114
|
+
// this input's sequence < 0xffffffff
|
|
115
|
+
if (sequence === undefined) {
|
|
116
|
+
sequence = rbf ? 0xfffffffd : 0xfffffffe;
|
|
117
|
+
}
|
|
118
|
+
else if (sequence > 0xfffffffe) {
|
|
119
|
+
throw new Error(`Error: incompatible sequence: ${sequence} and locktime: ${locktime}`);
|
|
120
|
+
}
|
|
121
|
+
if (sequence === undefined && rbf)
|
|
122
|
+
sequence = 0xfffffffd;
|
|
123
|
+
// Set locktime via scure's internal global field
|
|
124
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
125
|
+
psbt.global.fallbackLocktime = locktime;
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
if (sequence === undefined) {
|
|
129
|
+
if (rbf)
|
|
130
|
+
sequence = 0xfffffffd;
|
|
131
|
+
else
|
|
132
|
+
sequence = 0xffffffff;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// Build input in @scure/btc-signer format
|
|
136
|
+
const input = {
|
|
137
|
+
txid: hex.decode(txId),
|
|
138
|
+
index: vout
|
|
139
|
+
};
|
|
140
|
+
if (txHex !== undefined) {
|
|
141
|
+
input['nonWitnessUtxo'] = hex.decode(txHex);
|
|
142
|
+
}
|
|
143
|
+
if (tapInternalKey) {
|
|
144
|
+
//Taproot
|
|
145
|
+
const tapBip32Derivation = keysInfo
|
|
146
|
+
.filter((keyInfo) => keyInfo.pubkey && keyInfo.masterFingerprint && keyInfo.path)
|
|
147
|
+
.map((keyInfo) => {
|
|
148
|
+
const pubkey = keyInfo.pubkey;
|
|
149
|
+
if (!pubkey)
|
|
150
|
+
throw new Error(`key ${keyInfo.keyExpression} missing pubkey`);
|
|
151
|
+
return [
|
|
152
|
+
pubkey,
|
|
153
|
+
{
|
|
154
|
+
hashes: [], // Empty for tr(KEY) taproot key spend
|
|
155
|
+
der: {
|
|
156
|
+
fingerprint: readUInt32BE(keyInfo.masterFingerprint),
|
|
157
|
+
path: bip32Path(keyInfo.path)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
];
|
|
161
|
+
});
|
|
162
|
+
if (tapBip32Derivation.length)
|
|
163
|
+
input['tapBip32Derivation'] = tapBip32Derivation;
|
|
164
|
+
input['tapInternalKey'] = tapInternalKey;
|
|
165
|
+
//TODO: currently only single-key taproot supported.
|
|
166
|
+
if (tapBip32Derivation.length > 1)
|
|
167
|
+
throw new Error('Only single key taproot inputs are currently supported');
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
const bip32Derivation = keysInfo
|
|
171
|
+
.filter((keyInfo) => keyInfo.pubkey && keyInfo.masterFingerprint && keyInfo.path)
|
|
172
|
+
.map((keyInfo) => {
|
|
173
|
+
const pubkey = keyInfo.pubkey;
|
|
174
|
+
if (!pubkey)
|
|
175
|
+
throw new Error(`key ${keyInfo.keyExpression} missing pubkey`);
|
|
176
|
+
return [
|
|
177
|
+
pubkey,
|
|
178
|
+
{
|
|
179
|
+
fingerprint: readUInt32BE(keyInfo.masterFingerprint),
|
|
180
|
+
path: bip32Path(keyInfo.path)
|
|
181
|
+
}
|
|
182
|
+
];
|
|
183
|
+
});
|
|
184
|
+
if (bip32Derivation.length)
|
|
185
|
+
input['bip32Derivation'] = bip32Derivation;
|
|
186
|
+
}
|
|
187
|
+
if (isSegwit && txHex !== undefined) {
|
|
188
|
+
//There's no need to put both witnessUtxo and nonWitnessUtxo
|
|
189
|
+
input['witnessUtxo'] = { script: scriptPubKey, amount: BigInt(value) };
|
|
190
|
+
}
|
|
191
|
+
if (sequence !== undefined)
|
|
192
|
+
input['sequence'] = sequence;
|
|
193
|
+
if (witnessScript)
|
|
194
|
+
input['witnessScript'] = witnessScript;
|
|
195
|
+
if (redeemScript)
|
|
196
|
+
input['redeemScript'] = redeemScript;
|
|
197
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
198
|
+
psbt.addInput(input);
|
|
199
|
+
return psbt.inputsLength - 1;
|
|
200
|
+
}
|
package/dist/re.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export declare const reOriginPath: string;
|
|
2
|
+
export declare const reMasterFingerprint: string;
|
|
3
|
+
export declare const reOrigin: string;
|
|
4
|
+
export declare const reChecksum: string;
|
|
5
|
+
export declare const reNonSegwitPubKey: string;
|
|
6
|
+
export declare const reSegwitPubKey: string;
|
|
7
|
+
export declare const reTaprootPubKey: string;
|
|
8
|
+
export declare const reWIF: string;
|
|
9
|
+
export declare const reXpub: string;
|
|
10
|
+
export declare const reXprv: string;
|
|
11
|
+
export declare const rePath: string;
|
|
12
|
+
export declare const reXpubKey: string;
|
|
13
|
+
export declare const reXprvKey: string;
|
|
14
|
+
export declare const reNonSegwitKeyExp: string;
|
|
15
|
+
export declare const reSegwitKeyExp: string;
|
|
16
|
+
export declare const reTaprootKeyExp: string;
|
|
17
|
+
export declare const reNonSegwitSortedMulti: string;
|
|
18
|
+
export declare const reSegwitSortedMulti: string;
|
|
19
|
+
export declare const anchorStartAndEnd: (re: string) => string;
|
|
20
|
+
export declare const rePkAnchored: string;
|
|
21
|
+
export declare const reAddrAnchored: string;
|
|
22
|
+
export declare const rePkhAnchored: string;
|
|
23
|
+
export declare const reWpkhAnchored: string;
|
|
24
|
+
export declare const reShWpkhAnchored: string;
|
|
25
|
+
export declare const reTrSingleKeyAnchored: string;
|
|
26
|
+
export declare const reShSortedMultiAnchored: string;
|
|
27
|
+
export declare const reWshSortedMultiAnchored: string;
|
|
28
|
+
export declare const reShWshSortedMultiAnchored: string;
|
|
29
|
+
export declare const reShMiniscriptAnchored: string;
|
|
30
|
+
export declare const reShWshMiniscriptAnchored: string;
|
|
31
|
+
export declare const reWshMiniscriptAnchored: string;
|
package/dist/re.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Copyright (c) 2025 Jose-Luis Landabaso - https://bitcoinerlab.com
|
|
2
|
+
// Distributed under the MIT software license
|
|
3
|
+
import { CHECKSUM_CHARSET } from './checksum.js';
|
|
4
|
+
//Regular expressions cheat sheet:
|
|
5
|
+
//https://www.keycdn.com/support/regex-cheat-sheet
|
|
6
|
+
//hardened characters
|
|
7
|
+
const reHardened = String.raw `(['hH])`;
|
|
8
|
+
//a level is a series of integers followed (optional) by a hardener char
|
|
9
|
+
const reLevel = String.raw `(\d+${reHardened}?)`;
|
|
10
|
+
//a path component is a level followed by a slash "/" char
|
|
11
|
+
const rePathComponent = String.raw `(${reLevel}\/)`;
|
|
12
|
+
//A path formed by a series of path components that can be hardened: /2'/23H/23
|
|
13
|
+
export const reOriginPath = String.raw `(\/${rePathComponent}*${reLevel})`; //The "*" means: "match 0 or more of the previous"
|
|
14
|
+
//an origin is something like this: [d34db33f/44'/0'/0'] where the path is optional. The fingerPrint is 8 chars hex
|
|
15
|
+
export const reMasterFingerprint = String.raw `[0-9a-fA-F]{8}`;
|
|
16
|
+
export const reOrigin = String.raw `(\[${reMasterFingerprint}(${reOriginPath})?\])`;
|
|
17
|
+
export const reChecksum = String.raw `(#[${CHECKSUM_CHARSET}]{8})`;
|
|
18
|
+
//Something like this: 0252972572d465d016d4c501887b8df303eee3ed602c056b1eb09260dfa0da0ab2
|
|
19
|
+
//as explained here: github.com/bitcoin/bitcoin/blob/master/doc/descriptors.md#reference
|
|
20
|
+
const reCompressedPubKey = String.raw `((02|03)[0-9a-fA-F]{64})`;
|
|
21
|
+
const reUncompressedPubKey = String.raw `(04[0-9a-fA-F]{128})`;
|
|
22
|
+
const reXOnlyPubKey = String.raw `([0-9a-fA-F]{64})`;
|
|
23
|
+
export const reNonSegwitPubKey = String.raw `(${reCompressedPubKey}|${reUncompressedPubKey})`;
|
|
24
|
+
export const reSegwitPubKey = String.raw `(${reCompressedPubKey})`;
|
|
25
|
+
export const reTaprootPubKey = String.raw `(${reCompressedPubKey}|${reXOnlyPubKey})`;
|
|
26
|
+
//https://learnmeabitcoin.com/technical/wif
|
|
27
|
+
//5, K, L for mainnet, 5: uncompressed, {K, L}: compressed
|
|
28
|
+
//c, 9, testnet, c: compressed, 9: uncompressed
|
|
29
|
+
export const reWIF = String.raw `([5KLc9][1-9A-HJ-NP-Za-km-z]{50,51})`;
|
|
30
|
+
//x for mainnet, t for testnet
|
|
31
|
+
export const reXpub = String.raw `([xXtT]pub[1-9A-HJ-NP-Za-km-z]{79,108})`;
|
|
32
|
+
export const reXprv = String.raw `([xXtT]prv[1-9A-HJ-NP-Za-km-z]{79,108})`;
|
|
33
|
+
//reRangeLevel is like reLevel but using a wildcard "*"
|
|
34
|
+
const reRangeLevel = String.raw `(\*(${reHardened})?)`;
|
|
35
|
+
//A path can be finished with stuff like this: /23 or /23h or /* or /*'
|
|
36
|
+
export const rePath = String.raw `(\/(${rePathComponent})*(${reRangeLevel}|${reLevel}))`;
|
|
37
|
+
//rePath is optional (note the "zero"): Followed by zero or more /NUM or /NUM' path elements to indicate unhardened or hardened derivation steps between the fingerprint and the key or xpub/xprv root that follows
|
|
38
|
+
export const reXpubKey = String.raw `(${reXpub})(${rePath})?`;
|
|
39
|
+
export const reXprvKey = String.raw `(${reXprv})(${rePath})?`;
|
|
40
|
+
//actualKey is the keyExpression without optional origin
|
|
41
|
+
const reNonSegwitActualKey = String.raw `(${reXpubKey}|${reXprvKey}|${reNonSegwitPubKey}|${reWIF})`;
|
|
42
|
+
const reSegwitActualKey = String.raw `(${reXpubKey}|${reXprvKey}|${reSegwitPubKey}|${reWIF})`;
|
|
43
|
+
const reTaprootActualKey = String.raw `(${reXpubKey}|${reXprvKey}|${reTaprootPubKey}|${reWIF})`;
|
|
44
|
+
//reOrigin is optional: Optionally, key origin information, consisting of:
|
|
45
|
+
//Matches a key expression: wif, xpub, xprv or pubkey:
|
|
46
|
+
export const reNonSegwitKeyExp = String.raw `(${reOrigin})?(${reNonSegwitActualKey})`;
|
|
47
|
+
export const reSegwitKeyExp = String.raw `(${reOrigin})?(${reSegwitActualKey})`;
|
|
48
|
+
export const reTaprootKeyExp = String.raw `(${reOrigin})?(${reTaprootActualKey})`;
|
|
49
|
+
const rePk = String.raw `pk\((.*?)\)`; //Matches anything. We assert later in the code that the pubkey is valid.
|
|
50
|
+
const reAddr = String.raw `addr\((.*?)\)`; //Matches anything. We assert later in the code that the address is valid.
|
|
51
|
+
const rePkh = String.raw `pkh\(${reNonSegwitKeyExp}\)`;
|
|
52
|
+
const reWpkh = String.raw `wpkh\(${reSegwitKeyExp}\)`;
|
|
53
|
+
const reShWpkh = String.raw `sh\(wpkh\(${reSegwitKeyExp}\)\)`;
|
|
54
|
+
const reTrSingleKey = String.raw `tr\(${reTaprootKeyExp}\)`; // TODO: tr(KEY,TREE) not yet supported. TrSingleKey used for tr(KEY)
|
|
55
|
+
export const reNonSegwitSortedMulti = String.raw `sortedmulti\(((?:1|2|3|4|5|6|7|8|9|10|11|12|13|14|15|16|17|18|19|20)(?:,${reNonSegwitKeyExp})+)\)`;
|
|
56
|
+
export const reSegwitSortedMulti = String.raw `sortedmulti\(((?:1|2|3|4|5|6|7|8|9|10|11|12|13|14|15|16|17|18|19|20)(?:,${reSegwitKeyExp})+)\)`;
|
|
57
|
+
const reMiniscript = String.raw `(.*?)`; //Matches anything. We assert later in the code that miniscripts are valid and sane.
|
|
58
|
+
//RegExp makers:
|
|
59
|
+
const makeReSh = (re) => String.raw `sh\(${re}\)`;
|
|
60
|
+
const makeReWsh = (re) => String.raw `wsh\(${re}\)`;
|
|
61
|
+
const makeReShWsh = (re) => makeReSh(makeReWsh(re));
|
|
62
|
+
export const anchorStartAndEnd = (re) => String.raw `^${re}$`; //starts and finishes like re (not composable)
|
|
63
|
+
const composeChecksum = (re) => String.raw `${re}(${reChecksum})?`; //it's optional (note the "?")
|
|
64
|
+
export const rePkAnchored = anchorStartAndEnd(composeChecksum(rePk));
|
|
65
|
+
export const reAddrAnchored = anchorStartAndEnd(composeChecksum(reAddr));
|
|
66
|
+
export const rePkhAnchored = anchorStartAndEnd(composeChecksum(rePkh));
|
|
67
|
+
export const reWpkhAnchored = anchorStartAndEnd(composeChecksum(reWpkh));
|
|
68
|
+
export const reShWpkhAnchored = anchorStartAndEnd(composeChecksum(reShWpkh));
|
|
69
|
+
export const reTrSingleKeyAnchored = anchorStartAndEnd(composeChecksum(reTrSingleKey));
|
|
70
|
+
export const reShSortedMultiAnchored = anchorStartAndEnd(composeChecksum(makeReSh(reNonSegwitSortedMulti)));
|
|
71
|
+
export const reWshSortedMultiAnchored = anchorStartAndEnd(composeChecksum(makeReWsh(reSegwitSortedMulti)));
|
|
72
|
+
export const reShWshSortedMultiAnchored = anchorStartAndEnd(composeChecksum(makeReShWsh(reSegwitSortedMulti)));
|
|
73
|
+
export const reShMiniscriptAnchored = anchorStartAndEnd(composeChecksum(makeReSh(reMiniscript)));
|
|
74
|
+
export const reShWshMiniscriptAnchored = anchorStartAndEnd(composeChecksum(makeReShWsh(reMiniscript)));
|
|
75
|
+
export const reWshMiniscriptAnchored = anchorStartAndEnd(composeChecksum(makeReWsh(reMiniscript)));
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Network } from './networks.js';
|
|
2
|
+
import type { BIP32Interface } from './types.js';
|
|
3
|
+
/** @function */
|
|
4
|
+
export declare const pkhBIP32: ({ masterNode, network, keyPath, account, change, index, isPublic }: {
|
|
5
|
+
masterNode: BIP32Interface;
|
|
6
|
+
/** @default networks.bitcoin */
|
|
7
|
+
network?: Network;
|
|
8
|
+
account: number;
|
|
9
|
+
change?: number | undefined;
|
|
10
|
+
index?: number | undefined | "*";
|
|
11
|
+
keyPath?: string;
|
|
12
|
+
/**
|
|
13
|
+
* Compute an xpub or xprv
|
|
14
|
+
* @default true
|
|
15
|
+
*/
|
|
16
|
+
isPublic?: boolean;
|
|
17
|
+
}) => string;
|
|
18
|
+
/** @function */
|
|
19
|
+
export declare const shWpkhBIP32: ({ masterNode, network, keyPath, account, change, index, isPublic }: {
|
|
20
|
+
masterNode: BIP32Interface;
|
|
21
|
+
/** @default networks.bitcoin */
|
|
22
|
+
network?: Network;
|
|
23
|
+
account: number;
|
|
24
|
+
change?: number | undefined;
|
|
25
|
+
index?: number | undefined | "*";
|
|
26
|
+
keyPath?: string;
|
|
27
|
+
/**
|
|
28
|
+
* Compute an xpub or xprv
|
|
29
|
+
* @default true
|
|
30
|
+
*/
|
|
31
|
+
isPublic?: boolean;
|
|
32
|
+
}) => string;
|
|
33
|
+
/** @function */
|
|
34
|
+
export declare const wpkhBIP32: ({ masterNode, network, keyPath, account, change, index, isPublic }: {
|
|
35
|
+
masterNode: BIP32Interface;
|
|
36
|
+
/** @default networks.bitcoin */
|
|
37
|
+
network?: Network;
|
|
38
|
+
account: number;
|
|
39
|
+
change?: number | undefined;
|
|
40
|
+
index?: number | undefined | "*";
|
|
41
|
+
keyPath?: string;
|
|
42
|
+
/**
|
|
43
|
+
* Compute an xpub or xprv
|
|
44
|
+
* @default true
|
|
45
|
+
*/
|
|
46
|
+
isPublic?: boolean;
|
|
47
|
+
}) => string;
|
|
48
|
+
/** @function */
|
|
49
|
+
export declare const trBIP32: ({ masterNode, network, keyPath, account, change, index, isPublic }: {
|
|
50
|
+
masterNode: BIP32Interface;
|
|
51
|
+
/** @default networks.bitcoin */
|
|
52
|
+
network?: Network;
|
|
53
|
+
account: number;
|
|
54
|
+
change?: number | undefined;
|
|
55
|
+
index?: number | undefined | "*";
|
|
56
|
+
keyPath?: string;
|
|
57
|
+
/**
|
|
58
|
+
* Compute an xpub or xprv
|
|
59
|
+
* @default true
|
|
60
|
+
*/
|
|
61
|
+
isPublic?: boolean;
|
|
62
|
+
}) => string;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { networks } from './networks.js';
|
|
2
|
+
import { keyExpressionBIP32 } from './keyExpressions.js';
|
|
3
|
+
function assertStandardKeyPath(keyPath) {
|
|
4
|
+
// Regular expression to match "/change/index" or "/change/*" format
|
|
5
|
+
const regex = /^\/[01]\/(\d+|\*)$/;
|
|
6
|
+
if (!regex.test(keyPath)) {
|
|
7
|
+
throw new Error("Error: Key path must be in the format '/change/index', where change is either 0 or 1 and index is a non-negative integer.");
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
function standardExpressionsBIP32Maker(purpose, scriptTemplate) {
|
|
11
|
+
/**
|
|
12
|
+
* Computes the standard descriptor based on given parameters.
|
|
13
|
+
*
|
|
14
|
+
* You can define the output location either by:
|
|
15
|
+
* - Providing the full `keyPath` (e.g., "/0/2").
|
|
16
|
+
* OR
|
|
17
|
+
* - Specifying the `change` and `index` values separately (e.g., `{change:0, index:2}`).
|
|
18
|
+
*
|
|
19
|
+
* For ranged indexing, the `index` can be set as a wildcard '*'. For example:
|
|
20
|
+
* - `keyPath="/0/*"`
|
|
21
|
+
* OR
|
|
22
|
+
* - `{change:0, index:'*'}`.
|
|
23
|
+
*/
|
|
24
|
+
function standardScriptExpressionBIP32({ masterNode, network = networks.bitcoin, keyPath, account, change, index, isPublic = true }) {
|
|
25
|
+
const originPath = `/${purpose}'/${network.bech32 === networks.bitcoin.bech32 ? 0 : 1}'/${account}'`;
|
|
26
|
+
if (keyPath !== undefined)
|
|
27
|
+
assertStandardKeyPath(keyPath);
|
|
28
|
+
const keyExpression = keyExpressionBIP32({
|
|
29
|
+
masterNode,
|
|
30
|
+
originPath,
|
|
31
|
+
keyPath,
|
|
32
|
+
change,
|
|
33
|
+
index,
|
|
34
|
+
isPublic
|
|
35
|
+
});
|
|
36
|
+
return scriptTemplate.replace('KEYEXPRESSION', keyExpression);
|
|
37
|
+
}
|
|
38
|
+
return standardScriptExpressionBIP32;
|
|
39
|
+
}
|
|
40
|
+
/** @function */
|
|
41
|
+
export const pkhBIP32 = standardExpressionsBIP32Maker(44, 'pkh(KEYEXPRESSION)');
|
|
42
|
+
/** @function */
|
|
43
|
+
export const shWpkhBIP32 = standardExpressionsBIP32Maker(49, 'sh(wpkh(KEYEXPRESSION))');
|
|
44
|
+
/** @function */
|
|
45
|
+
export const wpkhBIP32 = standardExpressionsBIP32Maker(84, 'wpkh(KEYEXPRESSION)');
|
|
46
|
+
/** @function */
|
|
47
|
+
export const trBIP32 = standardExpressionsBIP32Maker(86, 'tr(KEYEXPRESSION)');
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export declare function varintEncodingLength(n: number): number;
|
|
2
|
+
/**
|
|
3
|
+
* Convert ASM string to script bytes.
|
|
4
|
+
* Applies minimal encoding rules matching bitcoinjs-lib: single-byte data
|
|
5
|
+
* pushes 0x01-0x10 are converted to OP_1-OP_16, and empty data is OP_0.
|
|
6
|
+
*/
|
|
7
|
+
export declare function fromASM(asm: string): Uint8Array;
|
|
8
|
+
/**
|
|
9
|
+
* Decompile script to array of opcodes and data.
|
|
10
|
+
* Returns opcode numbers and Uint8Array data pushes.
|
|
11
|
+
*/
|
|
12
|
+
export declare function decompileScript(script: Uint8Array): (number | Uint8Array)[] | null;
|
|
13
|
+
/**
|
|
14
|
+
* Encode a number for use in ASM.
|
|
15
|
+
* Returns a hex string for non-zero numbers, "OP_0" for zero.
|
|
16
|
+
*/
|
|
17
|
+
export declare function numberEncodeAsm(number: number): string;
|
|
18
|
+
/** Read a 4-byte big-endian unsigned integer from a Uint8Array. */
|
|
19
|
+
export declare function readUInt32BE(buf: Uint8Array): number;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// Copyright (c) 2025 Jose-Luis Landabaso - https://bitcoinerlab.com
|
|
2
|
+
// Distributed under the MIT software license
|
|
3
|
+
import * as btc from '@scure/btc-signer';
|
|
4
|
+
const { Script, OP, ScriptNum } = btc;
|
|
5
|
+
import { hex } from '@scure/base';
|
|
6
|
+
// ---- Varint encoding ----
|
|
7
|
+
export function varintEncodingLength(n) {
|
|
8
|
+
if (n < 0xfd)
|
|
9
|
+
return 1;
|
|
10
|
+
if (n <= 0xffff)
|
|
11
|
+
return 3;
|
|
12
|
+
if (n <= 0xffffffff)
|
|
13
|
+
return 5;
|
|
14
|
+
return 9;
|
|
15
|
+
}
|
|
16
|
+
// ---- Script helpers ----
|
|
17
|
+
// Build set of valid btc-signer opcode names
|
|
18
|
+
const SIGNER_OP_NAMES = new Set();
|
|
19
|
+
for (const key of Object.keys(OP)) {
|
|
20
|
+
if (isNaN(Number(key))) {
|
|
21
|
+
SIGNER_OP_NAMES.add(key);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function asmTokenToSignerOp(token) {
|
|
25
|
+
if (token === 'OP_0' || token === 'OP_FALSE')
|
|
26
|
+
return 'OP_0';
|
|
27
|
+
if (token === 'OP_1' || token === 'OP_TRUE')
|
|
28
|
+
return 'OP_1';
|
|
29
|
+
for (let i = 2; i <= 16; i++) {
|
|
30
|
+
if (token === `OP_${i}`)
|
|
31
|
+
return `OP_${i}`;
|
|
32
|
+
}
|
|
33
|
+
if (token === '1NEGATE' || token === 'OP_1NEGATE')
|
|
34
|
+
return '1NEGATE';
|
|
35
|
+
if (token.startsWith('OP_')) {
|
|
36
|
+
const stripped = token.slice(3);
|
|
37
|
+
if (SIGNER_OP_NAMES.has(stripped))
|
|
38
|
+
return stripped;
|
|
39
|
+
}
|
|
40
|
+
if (SIGNER_OP_NAMES.has(token))
|
|
41
|
+
return token;
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Convert ASM string to script bytes.
|
|
46
|
+
* Applies minimal encoding rules matching bitcoinjs-lib: single-byte data
|
|
47
|
+
* pushes 0x01-0x10 are converted to OP_1-OP_16, and empty data is OP_0.
|
|
48
|
+
*/
|
|
49
|
+
export function fromASM(asm) {
|
|
50
|
+
const tokens = asm.trim().split(/\s+/);
|
|
51
|
+
const scriptElements = [];
|
|
52
|
+
for (const token of tokens) {
|
|
53
|
+
if (token === '')
|
|
54
|
+
continue;
|
|
55
|
+
const op = asmTokenToSignerOp(token);
|
|
56
|
+
if (op !== undefined) {
|
|
57
|
+
scriptElements.push(op);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
try {
|
|
61
|
+
const data = hex.decode(token);
|
|
62
|
+
if (data.length === 0) {
|
|
63
|
+
scriptElements.push('OP_0');
|
|
64
|
+
}
|
|
65
|
+
else if (data.length === 1 && data[0] >= 1 && data[0] <= 16) {
|
|
66
|
+
scriptElements.push(`OP_${data[0]}`);
|
|
67
|
+
}
|
|
68
|
+
else if (data.length === 1 && data[0] === 0x81) {
|
|
69
|
+
scriptElements.push('1NEGATE');
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
scriptElements.push(data);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
throw new Error(`Error: unknown ASM token: ${token}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
81
|
+
return Script.encode(scriptElements);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Decompile script to array of opcodes and data.
|
|
85
|
+
* Returns opcode numbers and Uint8Array data pushes.
|
|
86
|
+
*/
|
|
87
|
+
export function decompileScript(script) {
|
|
88
|
+
try {
|
|
89
|
+
const decoded = Script.decode(script);
|
|
90
|
+
return decoded.map(item => {
|
|
91
|
+
if (typeof item === 'number')
|
|
92
|
+
return item;
|
|
93
|
+
if (item instanceof Uint8Array)
|
|
94
|
+
return item;
|
|
95
|
+
// String opcode: convert to number
|
|
96
|
+
const opNum = OP[item];
|
|
97
|
+
if (opNum !== undefined)
|
|
98
|
+
return opNum;
|
|
99
|
+
return item;
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Encode a number for use in ASM.
|
|
108
|
+
* Returns a hex string for non-zero numbers, "OP_0" for zero.
|
|
109
|
+
*/
|
|
110
|
+
export function numberEncodeAsm(number) {
|
|
111
|
+
if (Number.isSafeInteger(number) === false) {
|
|
112
|
+
throw new Error(`Error: invalid number ${number}`);
|
|
113
|
+
}
|
|
114
|
+
if (number === 0) {
|
|
115
|
+
return 'OP_0';
|
|
116
|
+
}
|
|
117
|
+
const encoded = ScriptNum(6).encode(BigInt(number));
|
|
118
|
+
return hex.encode(encoded);
|
|
119
|
+
}
|
|
120
|
+
// ---- Byte helpers ----
|
|
121
|
+
/** Read a 4-byte big-endian unsigned integer from a Uint8Array. */
|
|
122
|
+
export function readUInt32BE(buf) {
|
|
123
|
+
return ((buf[0] << 24) | (buf[1] << 16) | (buf[2] << 8) | buf[3]) >>> 0;
|
|
124
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { PsbtLike } from './psbt.js';
|
|
2
|
+
import type { ECPairInterface, BIP32Interface } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Signs a specific input of a PSBT with an ECPair.
|
|
5
|
+
*
|
|
6
|
+
* Uses @scure/btc-signer's signIdx which automatically handles
|
|
7
|
+
* Taproot key tweaking internally.
|
|
8
|
+
*
|
|
9
|
+
* @param {Object} params - The parameters object
|
|
10
|
+
* @param {PsbtLike} params.psbt - The PSBT to sign
|
|
11
|
+
* @param {number} params.index - The input index to sign
|
|
12
|
+
* @param {ECPairInterface} params.ecpair - The ECPair to sign with
|
|
13
|
+
*/
|
|
14
|
+
export declare function signInputECPair({ psbt, index, ecpair }: {
|
|
15
|
+
psbt: PsbtLike;
|
|
16
|
+
index: number;
|
|
17
|
+
ecpair: ECPairInterface;
|
|
18
|
+
}): void;
|
|
19
|
+
/**
|
|
20
|
+
* Signs all inputs of a PSBT with an ECPair.
|
|
21
|
+
*
|
|
22
|
+
* Uses @scure/btc-signer's sign which automatically handles
|
|
23
|
+
* Taproot key tweaking internally.
|
|
24
|
+
*
|
|
25
|
+
* @param {Object} params - The parameters object
|
|
26
|
+
* @param {PsbtLike} params.psbt - The PSBT to sign
|
|
27
|
+
* @param {ECPairInterface} params.ecpair - The ECPair to sign with
|
|
28
|
+
*/
|
|
29
|
+
export declare function signECPair({ psbt, ecpair }: {
|
|
30
|
+
psbt: PsbtLike;
|
|
31
|
+
ecpair: ECPairInterface;
|
|
32
|
+
}): void;
|
|
33
|
+
/**
|
|
34
|
+
* Signs a specific input of a PSBT with a BIP32 node.
|
|
35
|
+
*
|
|
36
|
+
* Handles Taproot inputs via tapBip32Derivation, and non-Taproot via
|
|
37
|
+
* @scure/btc-signer's signIdx with an HDKey adapter.
|
|
38
|
+
*
|
|
39
|
+
* @param {Object} params - The parameters object
|
|
40
|
+
* @param {PsbtLike} params.psbt - The PSBT to sign
|
|
41
|
+
* @param {number} params.index - The input index to sign
|
|
42
|
+
* @param {BIP32Interface} params.node - The BIP32 node to sign with
|
|
43
|
+
*/
|
|
44
|
+
export declare function signInputBIP32({ psbt, index, node }: {
|
|
45
|
+
psbt: PsbtLike;
|
|
46
|
+
index: number;
|
|
47
|
+
node: BIP32Interface;
|
|
48
|
+
}): void;
|
|
49
|
+
/**
|
|
50
|
+
* Signs all inputs of a PSBT with a BIP32 master node.
|
|
51
|
+
*
|
|
52
|
+
* First signs any Taproot inputs via tapBip32Derivation, then uses
|
|
53
|
+
* @scure/btc-signer's sign for remaining non-Taproot inputs.
|
|
54
|
+
*
|
|
55
|
+
* @param {Object} params - The parameters object
|
|
56
|
+
* @param {PsbtLike} params.psbt - The PSBT to sign
|
|
57
|
+
* @param {BIP32Interface} params.masterNode - The BIP32 master node to sign with
|
|
58
|
+
*/
|
|
59
|
+
export declare function signBIP32({ psbt, masterNode }: {
|
|
60
|
+
psbt: PsbtLike;
|
|
61
|
+
masterNode: BIP32Interface;
|
|
62
|
+
}): void;
|