@scure/btc-signer 2.0.1 → 2.3.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.
- package/README.md +334 -64
- package/index.d.ts +15 -6
- package/index.js +16 -7
- package/musig2.d.ts +212 -69
- package/musig2.js +352 -99
- package/net.d.ts +355 -0
- package/net.js +875 -0
- package/p2p.d.ts +17 -8
- package/p2p.js +63 -11
- package/package.json +17 -17
- package/payment.d.ts +406 -41
- package/payment.js +570 -69
- package/psbt.d.ts +2958 -560
- package/psbt.js +475 -119
- package/script.d.ts +311 -133
- package/script.js +313 -90
- package/src/_type_test.ts +69 -0
- package/src/index.ts +34 -11
- package/src/musig2.ts +424 -155
- package/src/net.ts +1106 -0
- package/src/p2p.ts +76 -24
- package/src/payment.ts +882 -235
- package/src/psbt.ts +648 -229
- package/src/script.ts +397 -139
- package/src/transaction.ts +667 -196
- package/src/utils.ts +392 -47
- package/src/utxo.ts +182 -83
- package/transaction.d.ts +242 -32
- package/transaction.js +531 -121
- package/utils.d.ts +296 -25
- package/utils.js +337 -30
- package/utxo.d.ts +438 -76
- package/utxo.js +150 -59
- package/index.d.ts.map +0 -1
- package/index.js.map +0 -1
- package/musig2.d.ts.map +0 -1
- package/musig2.js.map +0 -1
- package/p2p.d.ts.map +0 -1
- package/p2p.js.map +0 -1
- package/payment.d.ts.map +0 -1
- package/payment.js.map +0 -1
- package/psbt.d.ts.map +0 -1
- package/psbt.js.map +0 -1
- package/script.d.ts.map +0 -1
- package/script.js.map +0 -1
- package/transaction.d.ts.map +0 -1
- package/transaction.js.map +0 -1
- package/utils.d.ts.map +0 -1
- package/utils.js.map +0 -1
- package/utxo.d.ts.map +0 -1
- package/utxo.js.map +0 -1
package/src/payment.ts
CHANGED
|
@@ -1,43 +1,64 @@
|
|
|
1
1
|
import { bech32, bech32m, type Coder, createBase58check, hex } from '@scure/base';
|
|
2
|
+
import { anumber } from '@noble/hashes/utils.js';
|
|
3
|
+
import { abytes } from '@noble/curves/utils.js';
|
|
2
4
|
import * as P from 'micro-packed';
|
|
3
5
|
import { TaprootControlBlock, type TransactionInput } from './psbt.ts';
|
|
4
|
-
import { OpToNum, Script, type ScriptType, VarBytes } from './script.ts';
|
|
6
|
+
import { MAX_SCRIPT_BYTE_LENGTH, OpToNum, Script, type ScriptType, VarBytes } from './script.ts';
|
|
5
7
|
import * as u from './utils.ts';
|
|
6
|
-
import { type BTC_NETWORK, type Bytes, NETWORK } from './utils.ts';
|
|
8
|
+
import { type BTC_NETWORK, type Bytes, NETWORK, type TArg, type TRet } from './utils.ts';
|
|
7
9
|
|
|
8
10
|
// We need following items:
|
|
9
11
|
// - encode/decode output script
|
|
10
12
|
// - generate input script
|
|
11
13
|
// - generate address/output/redeem from user input
|
|
12
14
|
// P2ret represents generic interface for all p2* methods
|
|
15
|
+
/** Common shape returned by payment helper constructors. */
|
|
13
16
|
export type P2Ret = {
|
|
17
|
+
/** Payment-script tag such as `pkh`, `wpkh`, or `tr`. */
|
|
14
18
|
type: string;
|
|
19
|
+
/** Serialized output script for the payment descriptor. */
|
|
15
20
|
script: Bytes;
|
|
21
|
+
/** Encoded address when the script has a standard address form. */
|
|
16
22
|
address?: string;
|
|
23
|
+
/** Redeem script for wrapped script-hash descriptors. */
|
|
17
24
|
redeemScript?: Bytes;
|
|
25
|
+
/** Witness script for SegWit script-hash descriptors. */
|
|
18
26
|
witnessScript?: Bytes;
|
|
27
|
+
/** Hash committed by the output script when applicable. */
|
|
19
28
|
hash?: Bytes;
|
|
20
29
|
};
|
|
21
30
|
|
|
22
31
|
// Pay to Anchor (P2A)
|
|
32
|
+
// BIP433 Pay-to-Anchor witness program bytes; the scriptPubKey is `OP_1 <0x4e73>`.
|
|
33
|
+
const P2A_PROGRAM = /* @__PURE__ */ Uint8Array.from([0x4e, 0x73]);
|
|
23
34
|
type OutP2AType = { type: 'p2a'; script: Bytes };
|
|
24
35
|
const OutP2A: Coder<OptScript, OutP2AType | undefined> = {
|
|
25
|
-
encode(from: ScriptType): OutP2AType | undefined {
|
|
26
|
-
|
|
36
|
+
encode(from: TArg<ScriptType>): TRet<OutP2AType | undefined> {
|
|
37
|
+
// BIP433 defines P2A as the exact OP_1 <0x4e73> scriptPubKey.
|
|
38
|
+
if (
|
|
39
|
+
from.length !== 2 ||
|
|
40
|
+
from[0] !== 1 ||
|
|
41
|
+
!u.isBytes(from[1]) ||
|
|
42
|
+
!u.equalBytes(from[1], P2A_PROGRAM)
|
|
43
|
+
)
|
|
27
44
|
return;
|
|
28
|
-
return { type: 'p2a', script: Script.encode(from) }
|
|
45
|
+
return { type: 'p2a', script: Script.encode(from) } as TRet<OutP2AType | undefined>;
|
|
29
46
|
},
|
|
30
|
-
decode: (to: OutP2AType): OptScript => {
|
|
47
|
+
decode: (to: TArg<OutP2AType>): TRet<OptScript> => {
|
|
31
48
|
if (to.type !== 'p2a') return;
|
|
32
|
-
|
|
49
|
+
// The decoded object keeps `script` for caller convenience, but the `p2a`
|
|
50
|
+
// tag always canonicalizes back to the fixed BIP433 script.
|
|
51
|
+
return [1, Uint8Array.from(P2A_PROGRAM)] as TRet<OptScript>;
|
|
33
52
|
},
|
|
34
53
|
};
|
|
35
54
|
|
|
36
55
|
// Public Key (P2PK)
|
|
37
56
|
type OutPKType = { type: 'pk'; pubkey: Bytes };
|
|
57
|
+
/** Optional parsed script result used by output-script coders. */
|
|
38
58
|
export type OptScript = ScriptType | undefined;
|
|
39
59
|
|
|
40
|
-
function isValidPubkey(pub: Bytes
|
|
60
|
+
function isValidPubkey(pub: TArg<Bytes>, type: u.PubT): boolean {
|
|
61
|
+
// Payment coders use a boolean guard here and normalize validatePubkey failures to false.
|
|
41
62
|
try {
|
|
42
63
|
u.validatePubkey(pub, type);
|
|
43
64
|
return true;
|
|
@@ -47,7 +68,9 @@ function isValidPubkey(pub: Bytes, type: u.PubT): boolean {
|
|
|
47
68
|
}
|
|
48
69
|
|
|
49
70
|
const OutPK: Coder<OptScript, OutPKType | undefined> = {
|
|
50
|
-
encode(from: ScriptType): OutPKType | undefined {
|
|
71
|
+
encode(from: TArg<ScriptType>): TRet<OutPKType | undefined> {
|
|
72
|
+
// BIP380/BIP381 `pk(KEY)` only admits SEC1 ECDSA pubkeys here; x-only
|
|
73
|
+
// 32-byte CHECKSIG scripts are left for the later tapscript coders.
|
|
51
74
|
if (
|
|
52
75
|
from.length !== 2 ||
|
|
53
76
|
!u.isBytes(from[0]) ||
|
|
@@ -55,61 +78,89 @@ const OutPK: Coder<OptScript, OutPKType | undefined> = {
|
|
|
55
78
|
from[1] !== 'CHECKSIG'
|
|
56
79
|
)
|
|
57
80
|
return;
|
|
58
|
-
return { type: 'pk', pubkey: from[0] }
|
|
81
|
+
return { type: 'pk', pubkey: from[0] } as TRet<OutPKType | undefined>;
|
|
82
|
+
},
|
|
83
|
+
decode: (to: TArg<OutPKType>): TRet<OptScript> => {
|
|
84
|
+
if (to.type !== 'pk') return;
|
|
85
|
+
// OutScript validates `pk.pubkey` before this branch emits the canonical
|
|
86
|
+
// `<pubkey> CHECKSIG` script.
|
|
87
|
+
return [to.pubkey, 'CHECKSIG'] as TRet<OptScript>;
|
|
59
88
|
},
|
|
60
|
-
decode: (to: OutPKType): OptScript => (to.type === 'pk' ? [to.pubkey, 'CHECKSIG'] : undefined),
|
|
61
89
|
};
|
|
62
90
|
|
|
63
91
|
// Public Key Hash (P2PKH)
|
|
64
92
|
type OutPKHType = { type: 'pkh'; hash: Bytes };
|
|
65
93
|
const OutPKH: Coder<OptScript, OutPKHType | undefined> = {
|
|
66
|
-
encode(from: ScriptType): OutPKHType | undefined {
|
|
94
|
+
encode(from: TArg<ScriptType>): TRet<OutPKHType | undefined> {
|
|
67
95
|
if (from.length !== 5 || from[0] !== 'DUP' || from[1] !== 'HASH160' || !u.isBytes(from[2]))
|
|
68
96
|
return;
|
|
97
|
+
// Require the exact 20-byte HASH160 here so near-miss scripts fall through
|
|
98
|
+
// to OutUnknown instead of throwing in the OutScript validator on decode.
|
|
99
|
+
if (from[2].length !== 20) return;
|
|
69
100
|
if (from[3] !== 'EQUALVERIFY' || from[4] !== 'CHECKSIG') return;
|
|
70
|
-
return { type: 'pkh', hash: from[2] }
|
|
101
|
+
return { type: 'pkh', hash: from[2] } as TRet<OutPKHType | undefined>;
|
|
71
102
|
},
|
|
72
|
-
|
|
73
|
-
|
|
103
|
+
// OutScript validates `pkh.hash` before this branch emits the canonical
|
|
104
|
+
// `DUP HASH160 <hash> EQUALVERIFY CHECKSIG` script.
|
|
105
|
+
decode: (to: TArg<OutPKHType>): TRet<OptScript> =>
|
|
106
|
+
(to.type === 'pkh'
|
|
107
|
+
? ['DUP', 'HASH160', to.hash, 'EQUALVERIFY', 'CHECKSIG']
|
|
108
|
+
: undefined) as TRet<OptScript>,
|
|
74
109
|
};
|
|
75
110
|
// Script Hash (P2SH)
|
|
76
111
|
type OutSHType = { type: 'sh'; hash: Bytes };
|
|
77
112
|
const OutSH: Coder<OptScript, OutSHType | undefined> = {
|
|
78
|
-
encode(from: ScriptType): OutSHType | undefined {
|
|
113
|
+
encode(from: TArg<ScriptType>): TRet<OutSHType | undefined> {
|
|
79
114
|
if (from.length !== 3 || from[0] !== 'HASH160' || !u.isBytes(from[1]) || from[2] !== 'EQUAL')
|
|
80
115
|
return;
|
|
81
|
-
|
|
116
|
+
// Require the exact 20-byte HASH160 here so near-miss scripts fall through
|
|
117
|
+
// to OutUnknown instead of throwing in the OutScript validator on decode.
|
|
118
|
+
if (from[1].length !== 20) return;
|
|
119
|
+
return { type: 'sh', hash: from[1] } as TRet<OutSHType | undefined>;
|
|
82
120
|
},
|
|
83
|
-
|
|
84
|
-
|
|
121
|
+
// OutScript validates `sh.hash` before this branch emits the canonical
|
|
122
|
+
// `HASH160 <hash> EQUAL` script.
|
|
123
|
+
decode: (to: TArg<OutSHType>): TRet<OptScript> =>
|
|
124
|
+
(to.type === 'sh' ? ['HASH160', to.hash, 'EQUAL'] : undefined) as TRet<OptScript>,
|
|
85
125
|
};
|
|
86
126
|
|
|
87
127
|
// Witness Script Hash (P2WSH)
|
|
88
128
|
type OutWSHType = { type: 'wsh'; hash: Bytes };
|
|
89
129
|
const OutWSH: Coder<OptScript, OutWSHType | undefined> = {
|
|
90
|
-
encode(from: ScriptType): OutWSHType | undefined {
|
|
130
|
+
encode(from: TArg<ScriptType>): TRet<OutWSHType | undefined> {
|
|
91
131
|
if (from.length !== 2 || from[0] !== 0 || !u.isBytes(from[1])) return;
|
|
132
|
+
// BIP382 `wsh()` is specifically the version-0 32-byte witness program.
|
|
133
|
+
// Other witness versions stay with the later coders.
|
|
92
134
|
if (from[1].length !== 32) return;
|
|
93
|
-
return { type: 'wsh', hash: from[1] }
|
|
135
|
+
return { type: 'wsh', hash: from[1] } as TRet<OutWSHType | undefined>;
|
|
94
136
|
},
|
|
95
|
-
|
|
137
|
+
// OutScript validates `wsh.hash` before this branch emits the canonical
|
|
138
|
+
// version-0 32-byte witness program.
|
|
139
|
+
decode: (to: TArg<OutWSHType>): TRet<OptScript> =>
|
|
140
|
+
(to.type === 'wsh' ? [0, to.hash] : undefined) as TRet<OptScript>,
|
|
96
141
|
};
|
|
97
142
|
|
|
98
143
|
// Witness Public Key Hash (P2WPKH)
|
|
99
144
|
type OutWPKHType = { type: 'wpkh'; hash: Bytes };
|
|
100
145
|
const OutWPKH: Coder<OptScript, OutWPKHType | undefined> = {
|
|
101
|
-
encode(from: ScriptType): OutWPKHType | undefined {
|
|
146
|
+
encode(from: TArg<ScriptType>): TRet<OutWPKHType | undefined> {
|
|
102
147
|
if (from.length !== 2 || from[0] !== 0 || !u.isBytes(from[1])) return;
|
|
148
|
+
// BIP382 `wpkh()` is specifically the version-0 20-byte witness program.
|
|
149
|
+
// Compressed-key restrictions are enforced upstream, and other witness
|
|
150
|
+
// versions stay with the later coders.
|
|
103
151
|
if (from[1].length !== 20) return;
|
|
104
|
-
return { type: 'wpkh', hash: from[1] }
|
|
152
|
+
return { type: 'wpkh', hash: from[1] } as TRet<OutWPKHType | undefined>;
|
|
105
153
|
},
|
|
106
|
-
|
|
154
|
+
// OutScript validates `wpkh.hash` before this branch emits the canonical
|
|
155
|
+
// version-0 20-byte witness program.
|
|
156
|
+
decode: (to: TArg<OutWPKHType>): TRet<OptScript> =>
|
|
157
|
+
(to.type === 'wpkh' ? [0, to.hash] : undefined) as TRet<OptScript>,
|
|
107
158
|
};
|
|
108
159
|
|
|
109
160
|
// Multisig (P2MS)
|
|
110
161
|
type OutMSType = { type: 'ms'; pubkeys: Bytes[]; m: number };
|
|
111
162
|
const OutMS: Coder<OptScript, OutMSType | undefined> = {
|
|
112
|
-
encode(from: ScriptType): OutMSType | undefined {
|
|
163
|
+
encode(from: TArg<ScriptType>): TRet<OutMSType | undefined> {
|
|
113
164
|
const last = from.length - 1;
|
|
114
165
|
if (from[last] !== 'CHECKMULTISIG') return;
|
|
115
166
|
const m = from[0];
|
|
@@ -117,27 +168,48 @@ const OutMS: Coder<OptScript, OutMSType | undefined> = {
|
|
|
117
168
|
if (typeof m !== 'number' || typeof n !== 'number') return;
|
|
118
169
|
const pubkeys = from.slice(1, -2);
|
|
119
170
|
if (n !== pubkeys.length) return;
|
|
120
|
-
|
|
121
|
-
|
|
171
|
+
// Require valid ECDSA pubkeys and `0 < m <= n` here so near-miss
|
|
172
|
+
// CHECKMULTISIG scripts (garbage keys, degenerate 0-of-0) fall through to
|
|
173
|
+
// OutUnknown instead of throwing in the OutScript validator on decode.
|
|
174
|
+
// Script.decode only yields 0..16 for opcode numbers, so n <= 16 holds.
|
|
175
|
+
for (const pub of pubkeys) if (!u.isBytes(pub) || !isValidPubkey(pub, u.PubT.ecdsa)) return;
|
|
176
|
+
if (!Number.isSafeInteger(m) || m < 1 || m > n) return;
|
|
177
|
+
// We don't need n here because it is the same as pubkeys.length.
|
|
178
|
+
return { type: 'ms', m, pubkeys: pubkeys as Bytes[] } as TRet<OutMSType | undefined>;
|
|
122
179
|
},
|
|
123
180
|
// checkmultisig(n, ..pubkeys, m)
|
|
124
|
-
decode: (to: OutMSType): OptScript =>
|
|
125
|
-
|
|
181
|
+
decode: (to: TArg<OutMSType>): TRet<OptScript> =>
|
|
182
|
+
// OutScript validates multisig pubkeys and `0 < m <= n <= 16`.
|
|
183
|
+
// This branch only emits the canonical `m <pubkeys...> n CHECKMULTISIG`
|
|
184
|
+
// script.
|
|
185
|
+
(to.type === 'ms'
|
|
186
|
+
? [to.m, ...to.pubkeys, to.pubkeys.length, 'CHECKMULTISIG']
|
|
187
|
+
: undefined) as TRet<OptScript>,
|
|
126
188
|
};
|
|
127
189
|
// Taproot (P2TR)
|
|
128
190
|
type OutTRType = { type: 'tr'; pubkey: Bytes };
|
|
129
191
|
const OutTR: Coder<OptScript, OutTRType | undefined> = {
|
|
130
|
-
encode(from: ScriptType): OutTRType | undefined {
|
|
131
|
-
|
|
132
|
-
|
|
192
|
+
encode(from: TArg<ScriptType>): TRet<OutTRType | undefined> {
|
|
193
|
+
// BIP141 witness programs are `OP_0..OP_16` followed by a direct 2..40-byte push.
|
|
194
|
+
// BIP341 assigns native taproot meaning only to version 1 with a 32-byte x-only program;
|
|
195
|
+
// other OP_1 program lengths remain reserved future witness programs and should fall through.
|
|
196
|
+
if (from.length !== 2 || from[0] !== 1 || !u.isBytes(from[1]) || from[1].length !== 32) return;
|
|
197
|
+
// A 32-byte v1 program with an off-curve x coordinate is a fundable but
|
|
198
|
+
// taproot-unspendable output; classify it as unknown instead of throwing
|
|
199
|
+
// in the OutScript validator on decode.
|
|
200
|
+
if (!isValidPubkey(from[1], u.PubT.schnorr)) return;
|
|
201
|
+
return { type: 'tr', pubkey: from[1] } as TRet<OutTRType | undefined>;
|
|
133
202
|
},
|
|
134
|
-
|
|
203
|
+
// OutScript validates `tr.pubkey` before this branch emits the canonical
|
|
204
|
+
// version-1 32-byte witness program.
|
|
205
|
+
decode: (to: TArg<OutTRType>): TRet<OptScript> =>
|
|
206
|
+
(to.type === 'tr' ? [1, to.pubkey] : undefined) as TRet<OptScript>,
|
|
135
207
|
};
|
|
136
208
|
|
|
137
209
|
// Taproot N-of-N multisig (P2TR_NS)
|
|
138
210
|
type OutTRNSType = { type: 'tr_ns'; pubkeys: Bytes[] };
|
|
139
211
|
const OutTRNS: Coder<OptScript, OutTRNSType | undefined> = {
|
|
140
|
-
encode(from: ScriptType): OutTRNSType | undefined {
|
|
212
|
+
encode(from: TArg<ScriptType>): TRet<OutTRNSType | undefined> {
|
|
141
213
|
const last = from.length - 1;
|
|
142
214
|
if (from[last] !== 'CHECKSIG') return;
|
|
143
215
|
const pubkeys = [];
|
|
@@ -148,24 +220,32 @@ const OutTRNS: Coder<OptScript, OutTRNSType | undefined> = {
|
|
|
148
220
|
if (elm !== 'CHECKSIGVERIFY' || i === last - 1) return;
|
|
149
221
|
continue;
|
|
150
222
|
}
|
|
151
|
-
|
|
223
|
+
// Require actual Schnorr pubkeys here so near-miss `<bytes> CHECKSIG`
|
|
224
|
+
// scripts fall through to OutUnknown instead of failing later.
|
|
225
|
+
if (!u.isBytes(elm) || !isValidPubkey(elm, u.PubT.schnorr)) return;
|
|
152
226
|
pubkeys.push(elm);
|
|
153
227
|
}
|
|
154
|
-
|
|
228
|
+
// BIP342 "Using a k-of-k script for every combination" documents the shape
|
|
229
|
+
// `<pubkey_1> CHECKSIGVERIFY ... <pubkey_n> CHECKSIG`; this matcher only
|
|
230
|
+
// classifies that embedded-pubkey form, so bare CHECKSIG stays unknown.
|
|
231
|
+
if (!pubkeys.length) return;
|
|
232
|
+
return { type: 'tr_ns', pubkeys } as TRet<OutTRNSType | undefined>;
|
|
155
233
|
},
|
|
156
|
-
decode: (to: OutTRNSType): OptScript => {
|
|
234
|
+
decode: (to: TArg<OutTRNSType>): TRet<OptScript> => {
|
|
157
235
|
if (to.type !== 'tr_ns') return;
|
|
158
236
|
const out: ScriptType = [];
|
|
159
237
|
for (let i = 0; i < to.pubkeys.length - 1; i++) out.push(to.pubkeys[i], 'CHECKSIGVERIFY');
|
|
238
|
+
// This branch assumes at least one Schnorr pubkey; [] would otherwise emit
|
|
239
|
+
// `[undefined, CHECKSIG]` and only fail later in Script.encode.
|
|
160
240
|
out.push(to.pubkeys[to.pubkeys.length - 1], 'CHECKSIG');
|
|
161
|
-
return out
|
|
241
|
+
return out as TRet<OptScript>;
|
|
162
242
|
},
|
|
163
243
|
};
|
|
164
244
|
|
|
165
245
|
// Taproot M-of-N Multisig (P2TR_MS)
|
|
166
246
|
type OutTRMSType = { type: 'tr_ms'; pubkeys: Bytes[]; m: number };
|
|
167
247
|
const OutTRMS: Coder<OptScript, OutTRMSType | undefined> = {
|
|
168
|
-
encode(from: ScriptType): OutTRMSType | undefined {
|
|
248
|
+
encode(from: TArg<ScriptType>): TRet<OutTRMSType | undefined> {
|
|
169
249
|
const last = from.length - 1;
|
|
170
250
|
if (from[last] !== 'NUMEQUAL' || from[1] !== 'CHECKSIG') return;
|
|
171
251
|
const pubkeys = [];
|
|
@@ -173,37 +253,52 @@ const OutTRMS: Coder<OptScript, OutTRMSType | undefined> = {
|
|
|
173
253
|
if (typeof m !== 'number') return;
|
|
174
254
|
for (let i = 0; i < last - 1; i++) {
|
|
175
255
|
const elm = from[i];
|
|
256
|
+
// Structural mismatches should fall through to OutUnknown instead of
|
|
257
|
+
// throwing from the tr_ms matcher.
|
|
176
258
|
if (i & 1) {
|
|
177
|
-
if (elm !== (i === 1 ? 'CHECKSIG' : 'CHECKSIGADD'))
|
|
178
|
-
throw new Error('OutScript.encode/tr_ms: wrong element');
|
|
259
|
+
if (elm !== (i === 1 ? 'CHECKSIG' : 'CHECKSIGADD')) return;
|
|
179
260
|
continue;
|
|
180
261
|
}
|
|
181
|
-
|
|
262
|
+
// Require actual Schnorr pubkeys here (same as tr_ns) so near-miss
|
|
263
|
+
// CHECKSIGADD scripts fall through to OutUnknown instead of throwing
|
|
264
|
+
// in the OutScript validator on decode.
|
|
265
|
+
if (!u.isBytes(elm) || !isValidPubkey(elm, u.PubT.schnorr)) return;
|
|
182
266
|
pubkeys.push(elm);
|
|
183
267
|
}
|
|
184
|
-
|
|
268
|
+
if (!Number.isSafeInteger(m) || m < 1 || m > pubkeys.length || pubkeys.length > 999) return;
|
|
269
|
+
return { type: 'tr_ms', pubkeys, m } as TRet<OutTRMSType | undefined>;
|
|
185
270
|
},
|
|
186
|
-
decode: (to: OutTRMSType): OptScript => {
|
|
271
|
+
decode: (to: TArg<OutTRMSType>): TRet<OptScript> => {
|
|
187
272
|
if (to.type !== 'tr_ms') return;
|
|
188
273
|
const out: ScriptType = [to.pubkeys[0], 'CHECKSIG'];
|
|
189
274
|
for (let i = 1; i < to.pubkeys.length; i++) out.push(to.pubkeys[i], 'CHECKSIGADD');
|
|
275
|
+
// This branch assumes `m` was already validated as an integer ScriptNum;
|
|
276
|
+
// fractional JS numbers would otherwise serialize as a different threshold.
|
|
190
277
|
out.push(to.m, 'NUMEQUAL');
|
|
191
|
-
return out
|
|
278
|
+
return out as TRet<OptScript>;
|
|
192
279
|
},
|
|
193
280
|
};
|
|
194
281
|
|
|
195
282
|
// Unknown output type
|
|
196
283
|
type OutUnknownType = { type: 'unknown'; script: Bytes };
|
|
197
284
|
const OutUnknown: Coder<OptScript, OutUnknownType | undefined> = {
|
|
198
|
-
encode(from: ScriptType): OutUnknownType | undefined {
|
|
199
|
-
|
|
285
|
+
encode(from: TArg<ScriptType>): TRet<OutUnknownType | undefined> {
|
|
286
|
+
// This is the catch-all fallback for scripts no structured coder recognized,
|
|
287
|
+
// so earlier matchers must return `undefined` instead of throwing on mismatch.
|
|
288
|
+
// Because this reserializes the parsed Script AST, unknown scripts preserve
|
|
289
|
+
// semantics but not original non-minimal push spellings.
|
|
290
|
+
return { type: 'unknown', script: Script.encode(from) } as TRet<OutUnknownType | undefined>;
|
|
200
291
|
},
|
|
201
|
-
decode: (to: OutUnknownType): OptScript =>
|
|
202
|
-
|
|
292
|
+
decode: (to: TArg<OutUnknownType>): TRet<OptScript> =>
|
|
293
|
+
// This reparses `unknown.script` through the semantic Script codec, so raw
|
|
294
|
+
// bytes must still be syntactically parseable and may canonicalize on re-encode.
|
|
295
|
+
(to.type === 'unknown' ? Script.decode(to.script) : undefined) as TRet<OptScript>,
|
|
203
296
|
};
|
|
204
297
|
// /Payments
|
|
205
298
|
|
|
206
|
-
const OutScripts = [
|
|
299
|
+
const OutScripts = /* @__PURE__ */ (() => [
|
|
300
|
+
// Order is semantic: specific structured coders run first and the catch-all
|
|
301
|
+
// unknown fallback must stay last.
|
|
207
302
|
OutP2A,
|
|
208
303
|
OutPK,
|
|
209
304
|
OutPKH,
|
|
@@ -215,22 +310,26 @@ const OutScripts = [
|
|
|
215
310
|
OutTRNS,
|
|
216
311
|
OutTRMS,
|
|
217
312
|
OutUnknown,
|
|
218
|
-
];
|
|
313
|
+
])();
|
|
219
314
|
// TODO: we can support user supplied output scripts now
|
|
220
315
|
// - addOutScript
|
|
221
316
|
// - removeOutScript
|
|
222
317
|
// - We can do that as log we modify array in-place
|
|
223
318
|
// - Actually is very hard, since there is sign/finalize logic
|
|
224
|
-
|
|
319
|
+
// Raw composition of semantic Script parsing with the ordered output-script
|
|
320
|
+
// matcher; OutScript adds the higher-level validation layer on top.
|
|
321
|
+
const _OutScript = /* @__PURE__ */ (() => P.apply(Script, P.coders.match(OutScripts)))();
|
|
225
322
|
|
|
226
323
|
/*
|
|
227
324
|
* UNSAFE: Custom scripts: mostly ordinals, be very careful when crafting new scripts
|
|
228
325
|
* Only taproot supported for now.
|
|
229
|
-
* NOTE: we can use same to move finalization logic from Transaction,
|
|
326
|
+
* NOTE: we can use same to move finalization logic from Transaction,
|
|
327
|
+
* but it will significantly change audited code.
|
|
230
328
|
*/
|
|
231
329
|
|
|
232
330
|
type FinalizeSignature = [{ pubKey: Bytes; leafHash: Bytes }, Bytes];
|
|
233
331
|
type CustomScriptOut = { type: string } & Record<string, any>;
|
|
332
|
+
/** Custom taproot script coder/finalizer hook. */
|
|
234
333
|
export type CustomScript = Coder<OptScript, CustomScriptOut | undefined> & {
|
|
235
334
|
finalizeTaproot?: (
|
|
236
335
|
script: Bytes,
|
|
@@ -240,90 +339,174 @@ export type CustomScript = Coder<OptScript, CustomScriptOut | undefined> & {
|
|
|
240
339
|
};
|
|
241
340
|
|
|
242
341
|
// We can validate this once, because of packed & coders
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
342
|
+
/**
|
|
343
|
+
* Coder for recognized Bitcoin output scripts.
|
|
344
|
+
* @example
|
|
345
|
+
* Decode a serialized output script back into the tagged payment descriptor.
|
|
346
|
+
* ```ts
|
|
347
|
+
* import { OutScript, p2wpkh } from '@scure/btc-signer/payment.js';
|
|
348
|
+
* import { pubECDSA, randomPrivateKeyBytes } from '@scure/btc-signer/utils.js';
|
|
349
|
+
* const pay = p2wpkh(pubECDSA(randomPrivateKeyBytes()));
|
|
350
|
+
* OutScript.decode(pay.script);
|
|
351
|
+
* ```
|
|
352
|
+
*/
|
|
353
|
+
export const OutScript: TRet<
|
|
354
|
+
P.CoderType<
|
|
355
|
+
NonNullable<
|
|
356
|
+
| OutP2AType
|
|
357
|
+
| OutPKType
|
|
358
|
+
| OutPKHType
|
|
359
|
+
| OutSHType
|
|
360
|
+
| OutWSHType
|
|
361
|
+
| OutWPKHType
|
|
362
|
+
| OutMSType
|
|
363
|
+
| OutTRType
|
|
364
|
+
| OutTRNSType
|
|
365
|
+
| OutTRMSType
|
|
366
|
+
| OutUnknownType
|
|
367
|
+
| undefined
|
|
368
|
+
>
|
|
257
369
|
>
|
|
258
|
-
> =
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
}
|
|
370
|
+
> = /* @__PURE__ */ (() =>
|
|
371
|
+
Object.freeze(
|
|
372
|
+
P.validate(_OutScript, (i) => {
|
|
373
|
+
if (i.type === 'pk' && !isValidPubkey(i.pubkey, u.PubT.ecdsa))
|
|
374
|
+
throw new Error('OutScript/pk: wrong key');
|
|
375
|
+
if (
|
|
376
|
+
(i.type === 'pkh' || i.type === 'sh' || i.type === 'wpkh') &&
|
|
377
|
+
(!u.isBytes(i.hash) || i.hash.length !== 20)
|
|
378
|
+
)
|
|
379
|
+
throw new Error(`OutScript/${i.type}: wrong hash`);
|
|
380
|
+
if (i.type === 'wsh' && (!u.isBytes(i.hash) || i.hash.length !== 32))
|
|
381
|
+
throw new Error(`OutScript/wsh: wrong hash`);
|
|
382
|
+
if (i.type === 'tr' && (!u.isBytes(i.pubkey) || !isValidPubkey(i.pubkey, u.PubT.schnorr)))
|
|
383
|
+
throw new Error('OutScript/tr: wrong taproot public key');
|
|
384
|
+
if (i.type === 'ms' || i.type === 'tr_ns' || i.type === 'tr_ms')
|
|
385
|
+
if (!Array.isArray(i.pubkeys)) throw new Error('OutScript/multisig: wrong pubkeys array');
|
|
386
|
+
if (i.type === 'ms') {
|
|
387
|
+
const n = i.pubkeys.length;
|
|
388
|
+
for (const p of i.pubkeys)
|
|
389
|
+
if (!isValidPubkey(p, u.PubT.ecdsa)) throw new Error('OutScript/multisig: wrong pubkey');
|
|
390
|
+
// Range checks are not enough here: non-integer JS numbers like 1.5 would
|
|
391
|
+
// otherwise slip through and serialize as a different ScriptNum threshold.
|
|
392
|
+
anumber(i.m, 'm');
|
|
393
|
+
if (i.m <= 0 || n > 16 || i.m > n) throw new Error('OutScript/multisig: invalid params');
|
|
394
|
+
}
|
|
395
|
+
if (i.type === 'tr_ns' || i.type === 'tr_ms') {
|
|
396
|
+
for (const p of i.pubkeys)
|
|
397
|
+
if (!isValidPubkey(p, u.PubT.schnorr))
|
|
398
|
+
throw new Error(`OutScript/${i.type}: wrong pubkey`);
|
|
399
|
+
}
|
|
400
|
+
if (i.type === 'tr_ms') {
|
|
401
|
+
const n = i.pubkeys.length;
|
|
402
|
+
// BIP 342 keeps the 1000-element stack limit. This CHECKSIG/CHECKSIGADD form
|
|
403
|
+
// momentarily has n witness items plus one pushed pubkey on the stack, so n must stay <= 999.
|
|
404
|
+
anumber(i.m, 'm');
|
|
405
|
+
if (i.m <= 0 || n > 999 || i.m > n) throw new Error('OutScript/tr_ms: invalid params');
|
|
406
|
+
}
|
|
407
|
+
return i;
|
|
408
|
+
})
|
|
409
|
+
))() as TRet<
|
|
410
|
+
P.CoderType<
|
|
411
|
+
NonNullable<
|
|
412
|
+
| OutP2AType
|
|
413
|
+
| OutPKType
|
|
414
|
+
| OutPKHType
|
|
415
|
+
| OutSHType
|
|
416
|
+
| OutWSHType
|
|
417
|
+
| OutWPKHType
|
|
418
|
+
| OutMSType
|
|
419
|
+
| OutTRType
|
|
420
|
+
| OutTRNSType
|
|
421
|
+
| OutTRMSType
|
|
422
|
+
| OutUnknownType
|
|
423
|
+
| undefined
|
|
424
|
+
>
|
|
425
|
+
>
|
|
426
|
+
>;
|
|
427
|
+
/** Type of the output-script coder. */
|
|
288
428
|
export type OutScriptType = typeof OutScript;
|
|
429
|
+
// TRet-wrapping OutScript changes decode() to the normalized descriptor surface, but the local
|
|
430
|
+
// checkScript/Address caches still need an explicit alias that can carry the decode-side `undefined`.
|
|
431
|
+
type AddressValue = NonNullable<ReturnType<OutScriptType['decode']>>;
|
|
432
|
+
type OutScriptValue = AddressValue | undefined;
|
|
289
433
|
|
|
290
434
|
// Basic sanity check for scripts
|
|
291
|
-
function checkWSH(s: OutWSHType
|
|
435
|
+
function checkWSH(s: TArg<OutWSHType>, witnessScript: TArg<Bytes>) {
|
|
292
436
|
if (!u.equalBytes(s.hash, u.sha256(witnessScript)))
|
|
293
437
|
throw new Error('checkScript: wsh wrong witnessScript hash');
|
|
438
|
+
// BIP141 only requires the witnessScript hash match; the type-based rejects
|
|
439
|
+
// below are an extra descriptor sanity layer for BIP382 invalid-descriptor
|
|
440
|
+
// bullets `wpkh() nested in wsh()` and `wsh() nested in wsh()`.
|
|
294
441
|
const w = OutScript.decode(witnessScript);
|
|
295
442
|
if (w.type === 'tr' || w.type === 'tr_ns' || w.type === 'tr_ms')
|
|
296
443
|
throw new Error(`checkScript: P2${w.type} cannot be wrapped in P2SH`);
|
|
297
|
-
if (w.type === 'wpkh' || w.type === 'sh')
|
|
444
|
+
if (w.type === 'wpkh' || w.type === 'wsh' || w.type === 'sh')
|
|
298
445
|
throw new Error(`checkScript: P2${w.type} cannot be wrapped in P2WSH`);
|
|
299
446
|
}
|
|
300
447
|
|
|
301
|
-
|
|
448
|
+
/**
|
|
449
|
+
* Validates that nested redeem and witness scripts match their wrappers.
|
|
450
|
+
* @param script - top-level output script
|
|
451
|
+
* @param redeemScript - optional redeem script for P2SH wrappers
|
|
452
|
+
* @param witnessScript - optional witness script for P2WSH wrappers
|
|
453
|
+
* @throws If the script nesting is invalid or unsupported. {@link Error}
|
|
454
|
+
* @example
|
|
455
|
+
* Verify that wrapped scripts and hashes still match after custom edits.
|
|
456
|
+
* ```ts
|
|
457
|
+
* import { hex } from '@scure/base';
|
|
458
|
+
* import { checkScript, p2pkh, p2sh } from '@scure/btc-signer/payment.js';
|
|
459
|
+
* const wrapped = p2sh(
|
|
460
|
+
* p2pkh(hex.decode('0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'))
|
|
461
|
+
* );
|
|
462
|
+
* checkScript(wrapped.script, wrapped.redeemScript);
|
|
463
|
+
* ```
|
|
464
|
+
*/
|
|
465
|
+
export function checkScript(
|
|
466
|
+
script?: TArg<Bytes>,
|
|
467
|
+
redeemScript?: TArg<Bytes>,
|
|
468
|
+
witnessScript?: TArg<Bytes>
|
|
469
|
+
): void {
|
|
470
|
+
let hasWsh = false;
|
|
471
|
+
let r: OutScriptValue = undefined;
|
|
302
472
|
if (script) {
|
|
303
473
|
const s = OutScript.decode(script);
|
|
474
|
+
// BIP174 Data Signers Check For bullets: provided redeemScript must match
|
|
475
|
+
// the scriptPubKey, and provided witnessScript must match the scriptPubKey
|
|
476
|
+
// or redeemScript instead of being silently ignored as stray metadata.
|
|
304
477
|
// ms||pk maybe work, but there will be no address, hard to spend
|
|
305
478
|
if (s.type === 'tr_ns' || s.type === 'tr_ms' || s.type === 'ms' || s.type == 'pk')
|
|
306
479
|
throw new Error(`checkScript: non-wrapped ${s.type}`);
|
|
307
|
-
if (
|
|
480
|
+
if (redeemScript) {
|
|
481
|
+
if (s.type !== 'sh') throw new Error('checkScript: redeemScript without P2SH');
|
|
308
482
|
if (!u.equalBytes(s.hash, u.hash160(redeemScript)))
|
|
309
483
|
throw new Error('checkScript: sh wrong redeemScript hash');
|
|
310
|
-
|
|
311
|
-
if (r
|
|
484
|
+
r = OutScript.decode(redeemScript) as OutScriptValue;
|
|
485
|
+
if (r?.type === 'tr' || r?.type === 'tr_ns' || r?.type === 'tr_ms')
|
|
312
486
|
throw new Error(`checkScript: P2${r.type} cannot be wrapped in P2SH`);
|
|
313
487
|
// Not sure if this unspendable, but we cannot represent this via PSBT
|
|
314
|
-
if (r
|
|
488
|
+
if (r?.type === 'sh') throw new Error('checkScript: P2SH cannot be wrapped in P2SH');
|
|
489
|
+
}
|
|
490
|
+
if (s.type === 'wsh') {
|
|
491
|
+
hasWsh = true;
|
|
492
|
+
if (witnessScript) checkWSH(s, witnessScript);
|
|
315
493
|
}
|
|
316
|
-
if (s.type === 'wsh' && witnessScript) checkWSH(s, witnessScript);
|
|
317
494
|
}
|
|
318
495
|
if (redeemScript) {
|
|
319
|
-
|
|
320
|
-
if (r
|
|
496
|
+
if (r === undefined) r = OutScript.decode(redeemScript) as OutScriptValue;
|
|
497
|
+
if (r?.type === 'wsh') {
|
|
498
|
+
hasWsh = true;
|
|
499
|
+
if (witnessScript) checkWSH(r as TArg<OutWSHType>, witnessScript);
|
|
500
|
+
}
|
|
321
501
|
}
|
|
502
|
+
if (witnessScript && !hasWsh) throw new Error('checkScript: witnessScript without P2WSH');
|
|
322
503
|
}
|
|
323
504
|
|
|
324
|
-
function uniqPubkey(pubkeys: Bytes[]) {
|
|
505
|
+
function uniqPubkey(pubkeys: TArg<Bytes[]>) {
|
|
325
506
|
const map: Record<string, boolean> = {};
|
|
326
507
|
for (const pub of pubkeys) {
|
|
508
|
+
// Exact-byte duplicate filter only: BIP383 valid vectors still permit the
|
|
509
|
+
// same point to appear in compressed and uncompressed SEC1 form in multi().
|
|
327
510
|
const key = hex.encode(pub);
|
|
328
511
|
if (map[key]) throw new Error(`Multisig: non-uniq pubkey: ${pubkeys.map(hex.encode)}`);
|
|
329
512
|
map[key] = true;
|
|
@@ -333,15 +516,68 @@ function uniqPubkey(pubkeys: Bytes[]) {
|
|
|
333
516
|
// Also we use satisfies for additional check (ts 4.9+)
|
|
334
517
|
type Extends<T, U> = T extends U ? T : never;
|
|
335
518
|
|
|
336
|
-
|
|
337
|
-
export
|
|
519
|
+
/** Pay-to-public-key output descriptor. */
|
|
520
|
+
export type P2PK = {
|
|
521
|
+
/** Payment-script tag for pay-to-public-key outputs. */
|
|
522
|
+
type: 'pk';
|
|
523
|
+
/** Serialized `pubkey CHECKSIG` script. */
|
|
524
|
+
script: TRet<Bytes>;
|
|
525
|
+
};
|
|
526
|
+
/**
|
|
527
|
+
* Builds a pay-to-public-key script.
|
|
528
|
+
* @param pubkey - compressed or uncompressed ECDSA public key
|
|
529
|
+
* @param _network - unused network placeholder for API consistency
|
|
530
|
+
* @returns P2PK descriptor.
|
|
531
|
+
* @throws If the public key cannot be encoded as a P2PK output. {@link Error}
|
|
532
|
+
* @example
|
|
533
|
+
* Build a bare pay-to-public-key output.
|
|
534
|
+
* ```ts
|
|
535
|
+
* import { hex } from '@scure/base';
|
|
536
|
+
* import { p2pk } from '@scure/btc-signer/payment.js';
|
|
537
|
+
* p2pk(hex.decode('0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'));
|
|
538
|
+
* ```
|
|
539
|
+
*/
|
|
540
|
+
export const p2pk = (
|
|
541
|
+
pubkey: TArg<Bytes>,
|
|
542
|
+
_network: BTC_NETWORK = NETWORK
|
|
543
|
+
): TRet<Extends<P2PK, P2Ret>> => {
|
|
338
544
|
// network is unused
|
|
339
545
|
if (!isValidPubkey(pubkey, u.PubT.ecdsa)) throw new Error('P2PK: invalid publicKey');
|
|
340
|
-
return {
|
|
546
|
+
return {
|
|
547
|
+
type: 'pk',
|
|
548
|
+
script: OutScript.encode({ type: 'pk', pubkey }),
|
|
549
|
+
} as const as TRet<Extends<P2PK, P2Ret>>;
|
|
341
550
|
};
|
|
342
551
|
|
|
343
|
-
|
|
344
|
-
export
|
|
552
|
+
/** Pay-to-public-key-hash output descriptor. */
|
|
553
|
+
export type P2PKH = {
|
|
554
|
+
/** Payment-script tag for pay-to-public-key-hash outputs. */
|
|
555
|
+
type: 'pkh';
|
|
556
|
+
/** Serialized P2PKH script. */
|
|
557
|
+
script: TRet<Bytes>;
|
|
558
|
+
/** Base58Check address for the descriptor. */
|
|
559
|
+
address: string;
|
|
560
|
+
/** HASH160 committed by the script. */
|
|
561
|
+
hash: TRet<Bytes>;
|
|
562
|
+
};
|
|
563
|
+
/**
|
|
564
|
+
* Builds a P2PKH output from a public key.
|
|
565
|
+
* @param publicKey - compressed or uncompressed ECDSA public key bytes; HASH160 commits to the exact encoding
|
|
566
|
+
* @param network - address network parameters
|
|
567
|
+
* @returns P2PKH descriptor.
|
|
568
|
+
* @throws If the public key cannot be encoded as a P2PKH output. {@link Error}
|
|
569
|
+
* @example
|
|
570
|
+
* Build a classic pay-to-public-key-hash output.
|
|
571
|
+
* ```ts
|
|
572
|
+
* import { hex } from '@scure/base';
|
|
573
|
+
* import { p2pkh } from '@scure/btc-signer/payment.js';
|
|
574
|
+
* p2pkh(hex.decode('0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'));
|
|
575
|
+
* ```
|
|
576
|
+
*/
|
|
577
|
+
export const p2pkh = (
|
|
578
|
+
publicKey: TArg<Bytes>,
|
|
579
|
+
network: BTC_NETWORK = NETWORK
|
|
580
|
+
): TRet<Extends<P2PKH, P2Ret>> => {
|
|
345
581
|
if (!isValidPubkey(publicKey, u.PubT.ecdsa)) throw new Error('P2PKH: invalid publicKey');
|
|
346
582
|
const hash = u.hash160(publicKey);
|
|
347
583
|
return {
|
|
@@ -349,79 +585,164 @@ export const p2pkh = (publicKey: Bytes, network: BTC_NETWORK = NETWORK): Extends
|
|
|
349
585
|
script: OutScript.encode({ type: 'pkh', hash }),
|
|
350
586
|
address: Address(network).encode({ type: 'pkh', hash }),
|
|
351
587
|
hash,
|
|
352
|
-
} as const
|
|
588
|
+
} as const as TRet<Extends<P2PKH, P2Ret>>;
|
|
353
589
|
};
|
|
354
590
|
|
|
591
|
+
/** Shared fields for pay-to-script-hash outputs. */
|
|
355
592
|
export type P2SHBase = {
|
|
593
|
+
/** Payment-script tag for pay-to-script-hash outputs. */
|
|
356
594
|
type: 'sh';
|
|
357
|
-
|
|
358
|
-
|
|
595
|
+
/** Child script wrapped by the P2SH output. */
|
|
596
|
+
redeemScript: TRet<Bytes>;
|
|
597
|
+
/** Serialized P2SH script. */
|
|
598
|
+
script: TRet<Bytes>;
|
|
599
|
+
/** Base58Check address for the descriptor. */
|
|
359
600
|
address: string;
|
|
360
|
-
|
|
601
|
+
/** HASH160 committed by the script. */
|
|
602
|
+
hash: TRet<Bytes>;
|
|
361
603
|
};
|
|
362
|
-
|
|
604
|
+
/** P2SH descriptor with an embedded witness script. */
|
|
605
|
+
export type P2SHWithWitness = P2SHBase & { witnessScript: TRet<Bytes> };
|
|
606
|
+
/** P2SH descriptor without an embedded witness script. */
|
|
363
607
|
export type P2SHWithoutWitness = Omit<P2SHBase, 'witnessScript'>;
|
|
608
|
+
/** Conditional P2SH return type for wrapped scripts. */
|
|
364
609
|
export type P2SHReturn<T extends P2Ret> = T extends { witnessScript: Bytes }
|
|
365
610
|
? P2SHWithWitness
|
|
366
611
|
: P2SHWithoutWitness;
|
|
612
|
+
/**
|
|
613
|
+
* Wraps a child script inside P2SH.
|
|
614
|
+
* @param child - child payment descriptor to wrap
|
|
615
|
+
* @param network - address network parameters
|
|
616
|
+
* @returns P2SH descriptor preserving witness metadata when present.
|
|
617
|
+
* @throws If the wrapped script combination is invalid or unsupported. {@link Error}
|
|
618
|
+
* @example
|
|
619
|
+
* Wrap a child script in P2SH so it gets a base58 address form.
|
|
620
|
+
* ```ts
|
|
621
|
+
* import { hex } from '@scure/base';
|
|
622
|
+
* import { p2pk, p2sh, p2wsh } from '@scure/btc-signer/payment.js';
|
|
623
|
+
* p2sh(p2wsh(p2pk(hex.decode('0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'))));
|
|
624
|
+
* ```
|
|
625
|
+
*/
|
|
367
626
|
export const p2sh = <T extends P2Ret>(
|
|
368
|
-
child: T
|
|
627
|
+
child: TArg<T>,
|
|
369
628
|
network: BTC_NETWORK = NETWORK
|
|
370
|
-
): Extends<P2SHReturn<T>, P2Ret
|
|
629
|
+
): TRet<Extends<P2SHReturn<T>, P2Ret>> => {
|
|
630
|
+
u.validateObject(child as Record<string, any>, {}, {}, 'child');
|
|
371
631
|
// It is already tested inside noble-hashes and checkScript
|
|
372
|
-
|
|
373
|
-
|
|
632
|
+
// BIP16 redeemScripts are pushed by scriptSig, so anything over the 520-byte pushed-element
|
|
633
|
+
// limit would be fundable by HASH160 but unspendable once wrapped in P2SH.
|
|
634
|
+
const c = child as T;
|
|
635
|
+
const cs = c.script;
|
|
636
|
+
if (!u.isBytes(cs)) throw new Error(`Wrong script: ${typeof c.script}, expected Uint8Array`);
|
|
637
|
+
if (cs.length > MAX_SCRIPT_BYTE_LENGTH)
|
|
638
|
+
throw new Error(
|
|
639
|
+
`P2SH: redeemScript exceeds ${MAX_SCRIPT_BYTE_LENGTH}-byte push limit: len=${cs.length}`
|
|
640
|
+
);
|
|
374
641
|
const hash = u.hash160(cs);
|
|
375
|
-
const
|
|
376
|
-
|
|
377
|
-
|
|
642
|
+
const out = { type: 'sh', hash } as const;
|
|
643
|
+
const script = OutScript.encode(out);
|
|
644
|
+
const address = Address(network).encode(out);
|
|
645
|
+
checkScript(script, cs, c.witnessScript);
|
|
646
|
+
if (c.witnessScript) {
|
|
378
647
|
return {
|
|
379
648
|
type: 'sh',
|
|
380
649
|
redeemScript: cs,
|
|
381
|
-
script
|
|
382
|
-
address
|
|
650
|
+
script,
|
|
651
|
+
address,
|
|
383
652
|
hash,
|
|
384
|
-
witnessScript:
|
|
385
|
-
} as Extends<P2SHReturn<T>, P2Ret
|
|
653
|
+
witnessScript: c.witnessScript,
|
|
654
|
+
} as unknown as TRet<Extends<P2SHReturn<T>, P2Ret>>;
|
|
386
655
|
} else {
|
|
387
656
|
return {
|
|
388
657
|
type: 'sh',
|
|
389
658
|
redeemScript: cs,
|
|
390
|
-
script
|
|
391
|
-
address
|
|
659
|
+
script,
|
|
660
|
+
address,
|
|
392
661
|
hash,
|
|
393
|
-
} as Extends<P2SHReturn<T>, P2Ret
|
|
662
|
+
} as unknown as TRet<Extends<P2SHReturn<T>, P2Ret>>;
|
|
394
663
|
}
|
|
395
664
|
};
|
|
396
665
|
|
|
666
|
+
/** Pay-to-witness-script-hash descriptor. */
|
|
397
667
|
export type P2WSH = {
|
|
668
|
+
/** Payment-script tag for pay-to-witness-script-hash outputs. */
|
|
398
669
|
type: 'wsh';
|
|
399
|
-
|
|
400
|
-
|
|
670
|
+
/** Child script committed by the witness program. */
|
|
671
|
+
witnessScript: TRet<Bytes>;
|
|
672
|
+
/** Serialized P2WSH script. */
|
|
673
|
+
script: TRet<Bytes>;
|
|
674
|
+
/** Bech32 address for the descriptor. */
|
|
401
675
|
address: string;
|
|
402
|
-
|
|
676
|
+
/** SHA256 committed by the witness program. */
|
|
677
|
+
hash: TRet<Bytes>;
|
|
403
678
|
};
|
|
404
|
-
|
|
679
|
+
/**
|
|
680
|
+
* Wraps a child script inside native SegWit P2WSH.
|
|
681
|
+
* @param child - child payment descriptor to wrap
|
|
682
|
+
* @param network - address network parameters
|
|
683
|
+
* @returns P2WSH descriptor.
|
|
684
|
+
* @throws If the wrapped script combination is invalid or unsupported. {@link Error}
|
|
685
|
+
* @example
|
|
686
|
+
* Wrap a child script in native SegWit P2WSH.
|
|
687
|
+
* ```ts
|
|
688
|
+
* import { hex } from '@scure/base';
|
|
689
|
+
* import { p2pk, p2wsh } from '@scure/btc-signer/payment.js';
|
|
690
|
+
* p2wsh(p2pk(hex.decode('0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798')));
|
|
691
|
+
* ```
|
|
692
|
+
*/
|
|
693
|
+
export const p2wsh = (
|
|
694
|
+
child: TArg<P2Ret>,
|
|
695
|
+
network: BTC_NETWORK = NETWORK
|
|
696
|
+
): TRet<Extends<P2WSH, P2Ret>> => {
|
|
697
|
+
u.validateObject(child as Record<string, any>, {}, {}, 'child');
|
|
405
698
|
const cs = child.script;
|
|
406
699
|
if (!u.isBytes(cs)) throw new Error(`Wrong script: ${typeof cs}, expected Uint8Array`);
|
|
700
|
+
// BIP141 P2WSH says the witness "must consist of ... a serialized script (witnessScript)"
|
|
701
|
+
// and that witnessScript is limited to 10,000 bytes, so larger wrapped scripts must reject.
|
|
702
|
+
if (cs.length > 10000) throw new Error('P2WSH: witnessScript exceeds 10,000 bytes');
|
|
407
703
|
const hash = u.sha256(cs);
|
|
408
704
|
const script = OutScript.encode({ type: 'wsh', hash });
|
|
409
705
|
checkScript(script, undefined, cs);
|
|
410
706
|
return {
|
|
411
707
|
type: 'wsh',
|
|
412
708
|
witnessScript: cs,
|
|
413
|
-
script
|
|
709
|
+
script,
|
|
414
710
|
address: Address(network).encode({ type: 'wsh', hash }),
|
|
415
711
|
hash,
|
|
416
|
-
} as const
|
|
712
|
+
} as const as TRet<Extends<P2WSH, P2Ret>>;
|
|
417
713
|
};
|
|
418
714
|
|
|
419
|
-
|
|
715
|
+
/** Pay-to-witness-public-key-hash descriptor. */
|
|
716
|
+
export type P2WPKH = {
|
|
717
|
+
/** Payment-script tag for pay-to-witness-public-key-hash outputs. */
|
|
718
|
+
type: 'wpkh';
|
|
719
|
+
/** Serialized P2WPKH script. */
|
|
720
|
+
script: TRet<Bytes>;
|
|
721
|
+
/** Bech32 address for the descriptor. */
|
|
722
|
+
address: string;
|
|
723
|
+
/** HASH160 committed by the witness program. */
|
|
724
|
+
hash: TRet<Bytes>;
|
|
725
|
+
};
|
|
726
|
+
/**
|
|
727
|
+
* Builds a native SegWit P2WPKH output from a public key.
|
|
728
|
+
* @param publicKey - compressed ECDSA public key
|
|
729
|
+
* @param network - address network parameters
|
|
730
|
+
* @returns P2WPKH descriptor.
|
|
731
|
+
* @throws If the public key cannot be encoded as a P2WPKH output. {@link Error}
|
|
732
|
+
* @example
|
|
733
|
+
* Build a native SegWit pay-to-public-key-hash output.
|
|
734
|
+
* ```ts
|
|
735
|
+
* import { hex } from '@scure/base';
|
|
736
|
+
* import { p2wpkh } from '@scure/btc-signer/payment.js';
|
|
737
|
+
* p2wpkh(hex.decode('0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'));
|
|
738
|
+
* ```
|
|
739
|
+
*/
|
|
420
740
|
export const p2wpkh = (
|
|
421
|
-
publicKey: Bytes
|
|
741
|
+
publicKey: TArg<Bytes>,
|
|
422
742
|
network: BTC_NETWORK = NETWORK
|
|
423
|
-
): Extends<P2WPKH, P2Ret
|
|
743
|
+
): TRet<Extends<P2WPKH, P2Ret>> => {
|
|
424
744
|
if (!isValidPubkey(publicKey, u.PubT.ecdsa)) throw new Error('P2WPKH: invalid publicKey');
|
|
745
|
+
// BIP 143 default policy: version-0 witness programs MUST use 33-byte compressed ECDSA keys.
|
|
425
746
|
if (publicKey.length === 65) throw new Error('P2WPKH: uncompressed public key');
|
|
426
747
|
const hash = u.hash160(publicKey);
|
|
427
748
|
return {
|
|
@@ -429,30 +750,54 @@ export const p2wpkh = (
|
|
|
429
750
|
script: OutScript.encode({ type: 'wpkh', hash }),
|
|
430
751
|
address: Address(network).encode({ type: 'wpkh', hash }),
|
|
431
752
|
hash,
|
|
432
|
-
} as const
|
|
753
|
+
} as const as TRet<Extends<P2WPKH, P2Ret>>;
|
|
433
754
|
};
|
|
434
755
|
|
|
435
|
-
|
|
756
|
+
/** Bare multisig output descriptor. */
|
|
757
|
+
export type P2MS = {
|
|
758
|
+
/** Payment-script tag for bare multisig outputs. */
|
|
759
|
+
type: 'ms';
|
|
760
|
+
/** Serialized bare multisig script. */
|
|
761
|
+
script: TRet<Bytes>;
|
|
762
|
+
};
|
|
763
|
+
/**
|
|
764
|
+
* Builds a bare multisig script.
|
|
765
|
+
* @param m - number of required signatures
|
|
766
|
+
* @param pubkeys - participating public keys
|
|
767
|
+
* @param allowSamePubkeys - whether duplicate keys are allowed
|
|
768
|
+
* @returns P2MS descriptor.
|
|
769
|
+
* @throws If the multisig parameters are invalid. {@link Error}
|
|
770
|
+
* @example
|
|
771
|
+
* Build a bare multisig output script.
|
|
772
|
+
* ```ts
|
|
773
|
+
* import { hex } from '@scure/base';
|
|
774
|
+
* import { p2ms } from '@scure/btc-signer/payment.js';
|
|
775
|
+
* p2ms(1, [hex.decode('0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798')], true);
|
|
776
|
+
* ```
|
|
777
|
+
*/
|
|
436
778
|
export const p2ms = (
|
|
437
779
|
m: number,
|
|
438
|
-
pubkeys: Bytes[]
|
|
780
|
+
pubkeys: TArg<Bytes[]>,
|
|
439
781
|
allowSamePubkeys = false
|
|
440
|
-
): Extends<P2MS, P2Ret
|
|
782
|
+
): TRet<Extends<P2MS, P2Ret>> => {
|
|
783
|
+
// BIP 11 only standardized bare multisig up to 3 keys; this helper still permits up to 16
|
|
784
|
+
// because the same script shape is commonly wrapped by p2sh()/p2wsh() instead of used bare.
|
|
441
785
|
if (!allowSamePubkeys) uniqPubkey(pubkeys);
|
|
442
786
|
return {
|
|
443
787
|
type: 'ms',
|
|
444
788
|
script: OutScript.encode({ type: 'ms', pubkeys, m }),
|
|
445
|
-
} as const
|
|
789
|
+
} as const as TRet<Extends<P2MS, P2Ret>>;
|
|
446
790
|
};
|
|
447
791
|
|
|
792
|
+
/** Internal taproot hash tree without merkle paths. */
|
|
448
793
|
export type HashedTree =
|
|
449
794
|
| { type: 'leaf'; version?: number; script: Bytes; hash: Bytes }
|
|
450
795
|
| { type: 'branch'; left: HashedTree; right: HashedTree; hash: Bytes };
|
|
451
796
|
function checkTaprootScript(
|
|
452
|
-
script: Bytes
|
|
453
|
-
internalPubKey: Bytes
|
|
797
|
+
script: TArg<Bytes>,
|
|
798
|
+
internalPubKey: TArg<Bytes>,
|
|
454
799
|
allowUnknownOutputs = false,
|
|
455
|
-
customScripts?: CustomScript[]
|
|
800
|
+
customScripts?: TArg<CustomScript[]>
|
|
456
801
|
) {
|
|
457
802
|
const out = OutScript.decode(script);
|
|
458
803
|
if (out.type === 'unknown') {
|
|
@@ -460,9 +805,16 @@ function checkTaprootScript(
|
|
|
460
805
|
// disable custom. All custom scripts for taproot should have prefix 'tr_'
|
|
461
806
|
if (customScripts) {
|
|
462
807
|
const cs = P.apply(Script, P.coders.match(customScripts));
|
|
463
|
-
|
|
808
|
+
let c;
|
|
809
|
+
// match() throws when no custom coder matches; treat that as "not a custom
|
|
810
|
+
// script" so the allowUnknownOutputs escape below stays reachable.
|
|
811
|
+
try {
|
|
812
|
+
c = cs.decode(script);
|
|
813
|
+
} catch (e) {
|
|
814
|
+
c = undefined;
|
|
815
|
+
}
|
|
464
816
|
if (c !== undefined) {
|
|
465
|
-
if (
|
|
817
|
+
if (!u.astring(c.type, 'c.type').startsWith('tr_'))
|
|
466
818
|
throw new Error(`P2TR: invalid custom type=${c.type}`);
|
|
467
819
|
return;
|
|
468
820
|
}
|
|
@@ -492,25 +844,35 @@ function checkTaprootScript(
|
|
|
492
844
|
}
|
|
493
845
|
}
|
|
494
846
|
|
|
847
|
+
/** Taproot key-path descriptor. */
|
|
495
848
|
export type P2TR = {
|
|
849
|
+
/** Payment-script tag for taproot outputs. */
|
|
496
850
|
type: 'tr';
|
|
851
|
+
/** Serialized v1 witness-program script. */
|
|
497
852
|
script: Bytes;
|
|
853
|
+
/** Bech32m address for the descriptor. */
|
|
498
854
|
address: string;
|
|
855
|
+
/** Tweaked x-only output key committed by the address and script. */
|
|
499
856
|
tweakedPubkey: Bytes;
|
|
857
|
+
/** Internal x-only taproot key before tweaking. */
|
|
500
858
|
tapInternalKey: Bytes;
|
|
501
859
|
};
|
|
860
|
+
/** Taproot descriptor with a script tree attached. */
|
|
502
861
|
export type P2TR_TREE = P2TR & {
|
|
503
862
|
tapMerkleRoot: Bytes;
|
|
504
863
|
tapLeafScript: TransactionInput['tapLeafScript'];
|
|
505
864
|
leaves: TaprootLeaf[];
|
|
506
865
|
};
|
|
507
866
|
|
|
867
|
+
/** Node accepted when constructing a taproot script tree. */
|
|
508
868
|
export type TaprootNode = {
|
|
509
869
|
script: Bytes | string;
|
|
510
870
|
leafVersion?: number;
|
|
511
871
|
weight?: number;
|
|
512
872
|
} & Partial<P2TR_TREE>;
|
|
873
|
+
/** Recursive taproot tree input. */
|
|
513
874
|
export type TaprootScriptTree = TaprootNode | TaprootScriptTree[];
|
|
875
|
+
/** Flat list of weighted taproot leaves. */
|
|
514
876
|
export type TaprootScriptList = TaprootNode[];
|
|
515
877
|
type _TaprootTreeInternal = {
|
|
516
878
|
weight?: number;
|
|
@@ -518,7 +880,34 @@ type _TaprootTreeInternal = {
|
|
|
518
880
|
};
|
|
519
881
|
|
|
520
882
|
// Helper for generating binary tree from list, with weights
|
|
521
|
-
|
|
883
|
+
/**
|
|
884
|
+
* Converts a flat list of weighted leaves into a binary taproot tree.
|
|
885
|
+
* @param taprootList - weighted leaves to arrange
|
|
886
|
+
* @returns Binary taproot script tree.
|
|
887
|
+
* @throws If the list is empty and cannot describe any tree. {@link Error}
|
|
888
|
+
* @example
|
|
889
|
+
* Start from a flat weighted list, then let the helper build the binary tree shape.
|
|
890
|
+
* ```ts
|
|
891
|
+
* import { hex } from '@scure/base';
|
|
892
|
+
* import { p2tr_pk, taprootListToTree } from '@scure/btc-signer/payment.js';
|
|
893
|
+
* taprootListToTree([
|
|
894
|
+
* p2tr_pk(hex.decode('f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9')),
|
|
895
|
+
* p2tr_pk(hex.decode('dff1d77f2a671c5f36183726db2341be58feae1da2deced843240f7b502ba659')),
|
|
896
|
+
* ]);
|
|
897
|
+
* ```
|
|
898
|
+
*/
|
|
899
|
+
export function taprootListToTree(taprootList: TArg<TaprootScriptList>): TRet<TaprootScriptTree> {
|
|
900
|
+
u.aarray<TaprootScriptList[number]>(taprootList, 'taprootList', (leaf, title) => {
|
|
901
|
+
// p2tr reduces non-binary trees through this helper, so nested branch arrays are valid here.
|
|
902
|
+
if (Array.isArray(leaf)) return;
|
|
903
|
+
u.validateObject(leaf as Record<string, any>, {}, {}, title);
|
|
904
|
+
// This helper only arranges weighted tree nodes; p2tr validates leaf scripts while hashing.
|
|
905
|
+
if (leaf.weight !== undefined) anumber(leaf.weight, title + '.weight');
|
|
906
|
+
});
|
|
907
|
+
// Empty flat lists cannot represent a taproot script tree; omit the tree entirely for
|
|
908
|
+
// key-path-only outputs instead of passing [] here, otherwise this helper would return
|
|
909
|
+
// undefined and downstream taproot tree walkers would fail much later on a non-tree value.
|
|
910
|
+
if (!taprootList.length) throw new Error('taprootListToTree: empty tree');
|
|
522
911
|
// Clone input in order to not corrupt it
|
|
523
912
|
const lst = Array.from(taprootList) as _TaprootTreeInternal[];
|
|
524
913
|
// We have at least 2 elements => can create branch
|
|
@@ -537,17 +926,24 @@ export function taprootListToTree(taprootList: TaprootScriptList): TaprootScript
|
|
|
537
926
|
}
|
|
538
927
|
// At this point there is always 1 element in lst
|
|
539
928
|
const last = lst[0];
|
|
540
|
-
return (last?.childs || last) as TaprootScriptTree
|
|
929
|
+
return (last?.childs || last) as TRet<TaprootScriptTree>;
|
|
541
930
|
}
|
|
542
931
|
|
|
932
|
+
/** Taproot leaf with its merkle path. */
|
|
543
933
|
export type TaprootLeaf = {
|
|
934
|
+
/** Leaf marker inside the annotated taproot tree. */
|
|
544
935
|
type: 'leaf';
|
|
936
|
+
/** Tapleaf version committed by the merkle tree. */
|
|
545
937
|
version?: number;
|
|
938
|
+
/** Serialized leaf script. */
|
|
546
939
|
script: Bytes;
|
|
940
|
+
/** Tagged tapleaf hash for the script and version. */
|
|
547
941
|
hash: Bytes;
|
|
942
|
+
/** Merkle path hashes required for script-path spending. */
|
|
548
943
|
path: Bytes[];
|
|
549
944
|
};
|
|
550
945
|
|
|
946
|
+
/** Internal taproot tree annotated with merkle paths. */
|
|
551
947
|
export type HashedTreeWithPath =
|
|
552
948
|
| TaprootLeaf
|
|
553
949
|
| {
|
|
@@ -558,48 +954,59 @@ export type HashedTreeWithPath =
|
|
|
558
954
|
path: Bytes[];
|
|
559
955
|
};
|
|
560
956
|
|
|
561
|
-
function taprootAddPath(
|
|
957
|
+
function taprootAddPath(
|
|
958
|
+
tree: TArg<HashedTree>,
|
|
959
|
+
path: TArg<Bytes[]> = []
|
|
960
|
+
): TRet<HashedTreeWithPath> {
|
|
562
961
|
if (!tree) throw new Error(`taprootAddPath: empty tree`);
|
|
563
|
-
if (tree.type === 'leaf') return { ...tree, path }
|
|
962
|
+
if (tree.type === 'leaf') return { ...tree, path } as TRet<HashedTreeWithPath>;
|
|
564
963
|
if (tree.type !== 'branch') throw new Error(`taprootAddPath: wrong type=${tree}`);
|
|
565
964
|
return {
|
|
566
965
|
...tree,
|
|
567
966
|
path,
|
|
568
|
-
//
|
|
967
|
+
// BIP 341 control blocks serialize sibling hashes from leaf to root, so prepend the
|
|
968
|
+
// current sibling before descending into the child subtree.
|
|
569
969
|
left: taprootAddPath(tree.left, [tree.right.hash, ...path]),
|
|
570
970
|
right: taprootAddPath(tree.right, [tree.left.hash, ...path]),
|
|
571
|
-
}
|
|
971
|
+
} as TRet<HashedTreeWithPath>;
|
|
572
972
|
}
|
|
573
|
-
function taprootWalkTree(tree: HashedTreeWithPath): TaprootLeaf[] {
|
|
973
|
+
function taprootWalkTree(tree: TArg<HashedTreeWithPath>): TRet<TaprootLeaf[]> {
|
|
574
974
|
if (!tree) throw new Error(`taprootAddPath: empty tree`);
|
|
575
|
-
if (tree.type === 'leaf') return [tree]
|
|
975
|
+
if (tree.type === 'leaf') return [tree] as TRet<TaprootLeaf[]>;
|
|
576
976
|
if (tree.type !== 'branch') throw new Error(`taprootWalkTree: wrong type=${tree}`);
|
|
577
|
-
|
|
977
|
+
// Keep a stable left-to-right DFS leaf order when flattening the annotated tree.
|
|
978
|
+
return [...taprootWalkTree(tree.left), ...taprootWalkTree(tree.right)] as TRet<TaprootLeaf[]>;
|
|
578
979
|
}
|
|
579
980
|
|
|
580
981
|
function taprootHashTree(
|
|
581
|
-
tree: TaprootScriptTree
|
|
582
|
-
internalPubKey: Bytes
|
|
982
|
+
tree: TArg<TaprootScriptTree>,
|
|
983
|
+
internalPubKey: TArg<Bytes>,
|
|
583
984
|
allowUnknownOutputs = false,
|
|
584
|
-
customScripts?: CustomScript[]
|
|
585
|
-
): HashedTree {
|
|
586
|
-
if (
|
|
985
|
+
customScripts?: TArg<CustomScript[]>
|
|
986
|
+
): TRet<HashedTree> {
|
|
987
|
+
if (tree === undefined) throw new Error('taprootHashTree: empty tree');
|
|
988
|
+
if (!Array.isArray(tree) && !P.utils.isPlainObject(tree))
|
|
989
|
+
throw new TypeError('"tree" expected object or array, got type=' + typeof tree);
|
|
587
990
|
if (Array.isArray(tree) && tree.length === 1) tree = tree[0];
|
|
588
991
|
// Terminal node (leaf)
|
|
589
992
|
if (!Array.isArray(tree)) {
|
|
590
|
-
|
|
993
|
+
u.validateObject(tree as Record<string, any>, {}, {}, 'tree');
|
|
994
|
+
const version = tree.leafVersion;
|
|
995
|
+
const { script: leafScript } = tree;
|
|
591
996
|
// Earliest tree walk where we can validate tapScripts
|
|
592
997
|
if (tree.tapLeafScript || (tree.tapMerkleRoot && !u.equalBytes(tree.tapMerkleRoot, P.EMPTY)))
|
|
593
998
|
throw new Error('P2TR: tapRoot leafScript cannot have tree');
|
|
594
|
-
const script =
|
|
595
|
-
|
|
999
|
+
const script =
|
|
1000
|
+
typeof leafScript === 'string'
|
|
1001
|
+
? hex.decode(leafScript)
|
|
1002
|
+
: abytes(leafScript, undefined, 'tree.script');
|
|
596
1003
|
checkTaprootScript(script, internalPubKey, allowUnknownOutputs, customScripts);
|
|
597
1004
|
return {
|
|
598
1005
|
type: 'leaf',
|
|
599
1006
|
version,
|
|
600
1007
|
script,
|
|
601
|
-
hash: tapLeafHash(script, version),
|
|
602
|
-
}
|
|
1008
|
+
hash: tapLeafHash(script, tapLeafVersion(version)),
|
|
1009
|
+
} as TRet<HashedTree>;
|
|
603
1010
|
}
|
|
604
1011
|
// If tree / branch is not binary tree, convert it
|
|
605
1012
|
if (tree.length !== 2) tree = taprootListToTree(tree as TaprootNode[]) as TaprootNode[];
|
|
@@ -608,41 +1015,91 @@ function taprootHashTree(
|
|
|
608
1015
|
// Both nodes should exist
|
|
609
1016
|
const left = taprootHashTree(tree[0], internalPubKey, allowUnknownOutputs, customScripts);
|
|
610
1017
|
const right = taprootHashTree(tree[1], internalPubKey, allowUnknownOutputs, customScripts);
|
|
611
|
-
//
|
|
1018
|
+
// BIP 341 sorts TapBranch child hashes lexicographically for hashing, but the original
|
|
1019
|
+
// left/right structure still determines the control-block sibling paths for each leaf.
|
|
612
1020
|
let [lH, rH] = [left.hash, right.hash];
|
|
613
1021
|
if (u.compareBytes(rH, lH) === -1) [lH, rH] = [rH, lH];
|
|
614
|
-
return {
|
|
1022
|
+
return {
|
|
1023
|
+
type: 'branch',
|
|
1024
|
+
left,
|
|
1025
|
+
right,
|
|
1026
|
+
hash: u.tagSchnorr('TapBranch', lH, rH),
|
|
1027
|
+
} as TRet<HashedTree>;
|
|
615
1028
|
}
|
|
616
1029
|
|
|
1030
|
+
/** Default tapleaf version used by taproot script-path outputs before adding the parity bit. */
|
|
617
1031
|
export const TAP_LEAF_VERSION = 0xc0;
|
|
618
|
-
|
|
619
|
-
|
|
1032
|
+
const tapLeafVersion = (version: number | undefined): number => {
|
|
1033
|
+
if (version === undefined) return TAP_LEAF_VERSION;
|
|
1034
|
+
anumber(version, 'leafVersion');
|
|
1035
|
+
// BIP341 script-path validation defines the effective leaf version as `v = c[0] & 0xfe`
|
|
1036
|
+
// and says it cannot be odd or `0x50`; tapleaf hashes also serialize this as one byte.
|
|
1037
|
+
if (version > 0xfe || version === 0x50 || !!(version & 1))
|
|
1038
|
+
throw new Error(`P2TR: invalid leafVersion=${version}`);
|
|
1039
|
+
return version;
|
|
1040
|
+
};
|
|
1041
|
+
/**
|
|
1042
|
+
* Computes the tagged hash of a tapleaf script.
|
|
1043
|
+
* @param script - tapleaf script bytes
|
|
1044
|
+
* @param version - base even tapleaf version byte (for tapscript, `0xc0`)
|
|
1045
|
+
* @returns Tapleaf hash.
|
|
1046
|
+
* @throws If the tapleaf version is not a valid even one-byte version.
|
|
1047
|
+
* {@link Error}
|
|
1048
|
+
* @example
|
|
1049
|
+
* Hash a finalized tapscript leaf before placing it into a Merkle tree.
|
|
1050
|
+
* ```ts
|
|
1051
|
+
* tapLeafHash(new Uint8Array([0x51]));
|
|
1052
|
+
* ```
|
|
1053
|
+
*/
|
|
1054
|
+
export const tapLeafHash = (script: TArg<Bytes>, version: number = TAP_LEAF_VERSION): TRet<Bytes> =>
|
|
1055
|
+
u.tagSchnorr('TapLeaf', new Uint8Array([tapLeafVersion(version)]), VarBytes.encode(script));
|
|
620
1056
|
|
|
621
1057
|
// Works as key OR tree.
|
|
622
1058
|
// If we only have tree, need to add unspendable key, otherwise
|
|
623
1059
|
// complex multisig wallet can be spent by owner of key only. See TAPROOT_UNSPENDABLE_KEY
|
|
1060
|
+
/** Conditional taproot return type for key-only or tree-backed outputs. */
|
|
624
1061
|
export type P2TRRet<T> = T extends TaprootScriptTree ? P2TR_TREE : P2TR;
|
|
1062
|
+
/**
|
|
1063
|
+
* Builds a taproot output from an internal key and optional script tree.
|
|
1064
|
+
* @param internalPubKey - x-only internal public key, hex string, or `undefined` for script-only outputs
|
|
1065
|
+
* @param tree - optional taproot script tree
|
|
1066
|
+
* @param network - address network parameters
|
|
1067
|
+
* @param allowUnknownOutputs - whether unknown leaf scripts are allowed
|
|
1068
|
+
* @param customScripts - optional custom script codecs for taproot leaves
|
|
1069
|
+
* @returns Taproot descriptor with optional script-path metadata.
|
|
1070
|
+
* @throws If the internal key or taproot script tree is invalid. {@link Error}
|
|
1071
|
+
* @example
|
|
1072
|
+
* Combine script leaves into a final taproot output descriptor and address.
|
|
1073
|
+
* ```ts
|
|
1074
|
+
* import { hex } from '@scure/base';
|
|
1075
|
+
* import { p2tr, p2tr_pk } from '@scure/btc-signer/payment.js';
|
|
1076
|
+
* p2tr(
|
|
1077
|
+
* undefined,
|
|
1078
|
+
* [p2tr_pk(hex.decode('f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9'))]
|
|
1079
|
+
* );
|
|
1080
|
+
* ```
|
|
1081
|
+
*/
|
|
625
1082
|
export function p2tr(
|
|
626
|
-
internalPubKey: Bytes | string
|
|
1083
|
+
internalPubKey: TArg<Bytes | string>,
|
|
627
1084
|
tree?: undefined,
|
|
628
1085
|
network?: BTC_NETWORK,
|
|
629
1086
|
allowUnknownOutputs?: boolean,
|
|
630
|
-
customScripts?: CustomScript[]
|
|
631
|
-
): Extends<P2TR, P2Ret
|
|
1087
|
+
customScripts?: TArg<CustomScript[]>
|
|
1088
|
+
): TRet<Extends<P2TR, P2Ret>>;
|
|
632
1089
|
export function p2tr(
|
|
633
|
-
internalPubKey: Bytes | string
|
|
634
|
-
tree: TaprootScriptTree
|
|
1090
|
+
internalPubKey: TArg<Bytes | string | undefined>,
|
|
1091
|
+
tree: TArg<TaprootScriptTree>,
|
|
635
1092
|
network?: BTC_NETWORK,
|
|
636
1093
|
allowUnknownOutputs?: boolean,
|
|
637
|
-
customScripts?: CustomScript[]
|
|
638
|
-
): Extends<P2TR_TREE, P2Ret
|
|
1094
|
+
customScripts?: TArg<CustomScript[]>
|
|
1095
|
+
): TRet<Extends<P2TR_TREE, P2Ret>>;
|
|
639
1096
|
export function p2tr(
|
|
640
|
-
internalPubKey?: Bytes | string
|
|
641
|
-
tree?: TaprootScriptTree
|
|
1097
|
+
internalPubKey?: TArg<Bytes | string>,
|
|
1098
|
+
tree?: TArg<TaprootScriptTree>,
|
|
642
1099
|
network: BTC_NETWORK = NETWORK,
|
|
643
1100
|
allowUnknownOutputs = false,
|
|
644
|
-
customScripts?: CustomScript[]
|
|
645
|
-
): Extends<P2TR & Partial<P2TR_TREE>, P2Ret
|
|
1101
|
+
customScripts?: TArg<CustomScript[]>
|
|
1102
|
+
): TRet<Extends<P2TR & Partial<P2TR_TREE>, P2Ret>> {
|
|
646
1103
|
// Unspendable
|
|
647
1104
|
if (!internalPubKey && !tree) throw new Error('p2tr: should have pubKey or scriptTree (or both)');
|
|
648
1105
|
const pubKey =
|
|
@@ -656,14 +1113,21 @@ export function p2tr(
|
|
|
656
1113
|
);
|
|
657
1114
|
const tapMerkleRoot = hashedTree.hash;
|
|
658
1115
|
const [tweakedPubkey, parity] = u.taprootTweakPubkey(pubKey, tapMerkleRoot);
|
|
659
|
-
const
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
1116
|
+
const tapLeafScript: NonNullable<TransactionInput['tapLeafScript']> = [];
|
|
1117
|
+
const leaves = taprootWalkTree(hashedTree).map((l) => {
|
|
1118
|
+
const version = tapLeafVersion(l.version);
|
|
1119
|
+
// Leaf versions are stored as the base even byte; only the control block adds the
|
|
1120
|
+
// output-key parity bit required by BIP 341 script-path spending.
|
|
1121
|
+
const controlBlock = {
|
|
1122
|
+
version: version + parity,
|
|
663
1123
|
internalKey: pubKey,
|
|
664
1124
|
merklePath: l.path,
|
|
665
|
-
}
|
|
666
|
-
|
|
1125
|
+
};
|
|
1126
|
+
// Skip an encode/decode copy for performance; callers must treat returned metadata as
|
|
1127
|
+
// immutable.
|
|
1128
|
+
tapLeafScript.push([controlBlock, u.concatBytes(l.script, new Uint8Array([version]))]);
|
|
1129
|
+
return { ...l, controlBlock: TaprootControlBlock.encode(controlBlock) };
|
|
1130
|
+
});
|
|
667
1131
|
return {
|
|
668
1132
|
type: 'tr',
|
|
669
1133
|
script: OutScript.encode({ type: 'tr', pubkey: tweakedPubkey }),
|
|
@@ -673,13 +1137,12 @@ export function p2tr(
|
|
|
673
1137
|
// PSBT stuff
|
|
674
1138
|
tapInternalKey: pubKey,
|
|
675
1139
|
leaves,
|
|
676
|
-
tapLeafScript
|
|
677
|
-
TaprootControlBlock.decode(l.controlBlock),
|
|
678
|
-
u.concatBytes(l.script, new Uint8Array([l.version || TAP_LEAF_VERSION])),
|
|
679
|
-
]),
|
|
1140
|
+
tapLeafScript,
|
|
680
1141
|
tapMerkleRoot,
|
|
681
|
-
} as const
|
|
1142
|
+
} as const as TRet<Extends<P2TR_TREE, P2Ret>>;
|
|
682
1143
|
} else {
|
|
1144
|
+
// BIP 341 / BIP 86: key-only Taproot still tweaks with the empty Merkle root so the
|
|
1145
|
+
// output commits to an unspendable script path instead of leaving the key untweaked.
|
|
683
1146
|
const tweakedPubkey = u.taprootTweakPubkey(pubKey, P.EMPTY)[0];
|
|
684
1147
|
return {
|
|
685
1148
|
type: 'tr',
|
|
@@ -689,21 +1152,36 @@ export function p2tr(
|
|
|
689
1152
|
tweakedPubkey,
|
|
690
1153
|
// PSBT stuff
|
|
691
1154
|
tapInternalKey: pubKey,
|
|
692
|
-
} as const
|
|
1155
|
+
} as const as TRet<Extends<P2TR, P2Ret>>;
|
|
693
1156
|
}
|
|
694
1157
|
}
|
|
695
1158
|
|
|
696
1159
|
// Returns all combinations of size M from lst
|
|
1160
|
+
/**
|
|
1161
|
+
* Returns all size-`m` combinations from a list.
|
|
1162
|
+
* @param m - size of each combination
|
|
1163
|
+
* @param list - input items to combine
|
|
1164
|
+
* @returns Array of combinations.
|
|
1165
|
+
* @throws If the combination size or input list is invalid. {@link Error}
|
|
1166
|
+
* @example
|
|
1167
|
+
* Enumerate all size-two subsets of a short list.
|
|
1168
|
+
* ```ts
|
|
1169
|
+
* combinations(2, [1, 2, 3]);
|
|
1170
|
+
* ```
|
|
1171
|
+
*/
|
|
697
1172
|
export function combinations<T>(m: number, list: T[]): T[][] {
|
|
698
1173
|
const res: T[][] = [];
|
|
699
1174
|
if (!Array.isArray(list)) throw new Error('combinations: lst arg should be array');
|
|
700
1175
|
const n = list.length;
|
|
701
|
-
|
|
1176
|
+
anumber(m, 'm');
|
|
1177
|
+
if (m < 1 || m > n) throw new Error('combinations: m must satisfy 1 <= m <= lst.length');
|
|
702
1178
|
/*
|
|
703
1179
|
Basically works as M nested loops like:
|
|
704
1180
|
for (;idx[0]<lst.length;idx[0]++) for (idx[1]=idx[0]+1;idx[1]<lst.length;idx[1]++)
|
|
705
1181
|
but since we cannot create nested loops dynamically, we unroll it to a single loop
|
|
706
1182
|
*/
|
|
1183
|
+
// This unrolled-loop implementation assumes an integer 1 <= m <= n; zero, negative, and
|
|
1184
|
+
// fractional m values need an explicit guard before entering the loop.
|
|
707
1185
|
const idx = Array.from({ length: m }, (_, i) => i);
|
|
708
1186
|
const last = idx.length - 1;
|
|
709
1187
|
main: for (;;) {
|
|
@@ -727,128 +1205,290 @@ export function combinations<T>(m: number, list: T[]): T[][] {
|
|
|
727
1205
|
/**
|
|
728
1206
|
* M-of-N multi-leaf wallet via p2tr_ns. If m == n, single script is emitted.
|
|
729
1207
|
* Takes O(n^2) if m != n. 99-of-100 is ok, 5-of-100 is not.
|
|
1208
|
+
* It materializes C(n, m) leaves, so middle-of-the-range thresholds blow up combinatorially.
|
|
730
1209
|
* `2-of-[A,B,C] => [A,B] | [A,C] | [B,C]`
|
|
731
1210
|
*/
|
|
732
|
-
export type P2TR_NS = {
|
|
1211
|
+
export type P2TR_NS = {
|
|
1212
|
+
/** Payment-script tag for taproot `CHECKSIGVERIFY` leaf scripts. */
|
|
1213
|
+
type: 'tr_ns';
|
|
1214
|
+
/** Serialized tapscript leaf. */
|
|
1215
|
+
script: TRet<Bytes>;
|
|
1216
|
+
};
|
|
1217
|
+
/**
|
|
1218
|
+
* Builds the leaf set for an M-of-N `CHECKSIGVERIFY` taproot policy.
|
|
1219
|
+
* @param m - number of required signatures
|
|
1220
|
+
* @param pubkeys - participating Schnorr public keys
|
|
1221
|
+
* @param allowSamePubkeys - whether duplicate keys are allowed
|
|
1222
|
+
* @returns Array of taproot leaf descriptors.
|
|
1223
|
+
* @throws If the taproot multisig parameters are invalid. {@link Error}
|
|
1224
|
+
* @example
|
|
1225
|
+
* Build the leaf set for an M-of-N taproot `CHECKSIGVERIFY` policy.
|
|
1226
|
+
* ```ts
|
|
1227
|
+
* import { hex } from '@scure/base';
|
|
1228
|
+
* import { p2tr_ns } from '@scure/btc-signer/payment.js';
|
|
1229
|
+
* p2tr_ns(1, [hex.decode('f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9')], true);
|
|
1230
|
+
* ```
|
|
1231
|
+
*/
|
|
733
1232
|
export const p2tr_ns = (
|
|
734
1233
|
m: number,
|
|
735
|
-
pubkeys: Bytes[]
|
|
1234
|
+
pubkeys: TArg<Bytes[]>,
|
|
736
1235
|
allowSamePubkeys = false
|
|
737
|
-
): Extends<P2TR_NS, P2Ret>[] => {
|
|
1236
|
+
): TRet<Extends<P2TR_NS, P2Ret>[]> => {
|
|
738
1237
|
if (!allowSamePubkeys) uniqPubkey(pubkeys);
|
|
739
1238
|
return combinations(m, pubkeys).map(
|
|
740
1239
|
(i) =>
|
|
741
1240
|
({
|
|
742
1241
|
type: 'tr_ns',
|
|
743
1242
|
script: OutScript.encode({ type: 'tr_ns', pubkeys: i }),
|
|
744
|
-
}) as const
|
|
745
|
-
)
|
|
1243
|
+
}) as const as TRet<Extends<P2TR_NS, P2Ret>>
|
|
1244
|
+
) as TRet<Extends<P2TR_NS, P2Ret>[]>;
|
|
746
1245
|
};
|
|
747
1246
|
// Taproot public key (case of p2tr_ns)
|
|
1247
|
+
/** Single-key taproot leaf descriptor. */
|
|
748
1248
|
export type P2TR_PK = P2TR_NS;
|
|
749
|
-
|
|
750
|
-
|
|
1249
|
+
/**
|
|
1250
|
+
* Builds a single-key taproot leaf script.
|
|
1251
|
+
* BIP 341 design guidance: if this is the most likely single-key spend path, prefer
|
|
1252
|
+
* using that key as the `p2tr()` internal key instead of forcing it into a script leaf.
|
|
1253
|
+
* @param pubkey - Schnorr public key
|
|
1254
|
+
* @returns Taproot single-key leaf descriptor.
|
|
1255
|
+
* @throws If the taproot single-key leaf cannot be encoded. {@link Error}
|
|
1256
|
+
* @example
|
|
1257
|
+
* Build a single-key tapscript leaf.
|
|
1258
|
+
* ```ts
|
|
1259
|
+
* import { hex } from '@scure/base';
|
|
1260
|
+
* import { p2tr_pk } from '@scure/btc-signer/payment.js';
|
|
1261
|
+
* p2tr_pk(hex.decode('f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9'));
|
|
1262
|
+
* ```
|
|
1263
|
+
*/
|
|
1264
|
+
export const p2tr_pk = (pubkey: TArg<Bytes>): TRet<Extends<P2TR_PK, P2Ret>> =>
|
|
1265
|
+
p2tr_ns(1, [pubkey], undefined)[0];
|
|
751
1266
|
|
|
752
|
-
|
|
1267
|
+
/** Taproot `CHECKSIGADD` multisig leaf descriptor. */
|
|
1268
|
+
export type P2TR_MS = {
|
|
1269
|
+
/** Payment-script tag for taproot `CHECKSIGADD` leaf scripts. */
|
|
1270
|
+
type: 'tr_ms';
|
|
1271
|
+
/** Serialized tapscript leaf. */
|
|
1272
|
+
script: TRet<Bytes>;
|
|
1273
|
+
};
|
|
1274
|
+
/**
|
|
1275
|
+
* Builds a `CHECKSIGADD` taproot multisig leaf.
|
|
1276
|
+
* @param m - number of required signatures
|
|
1277
|
+
* @param pubkeys - participating Schnorr public keys
|
|
1278
|
+
* @param allowSamePubkeys - whether duplicate keys are allowed
|
|
1279
|
+
* @returns Taproot multisig leaf descriptor.
|
|
1280
|
+
* @throws If the taproot multisig parameters are invalid. {@link Error}
|
|
1281
|
+
* @example
|
|
1282
|
+
* Build a `CHECKSIGADD` taproot multisig leaf.
|
|
1283
|
+
* ```ts
|
|
1284
|
+
* import { hex } from '@scure/base';
|
|
1285
|
+
* import { p2tr_ms } from '@scure/btc-signer/payment.js';
|
|
1286
|
+
* p2tr_ms(1, [hex.decode('f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9')], true);
|
|
1287
|
+
* ```
|
|
1288
|
+
*/
|
|
753
1289
|
export function p2tr_ms(
|
|
754
1290
|
m: number,
|
|
755
|
-
pubkeys: Bytes[]
|
|
1291
|
+
pubkeys: TArg<Bytes[]>,
|
|
756
1292
|
allowSamePubkeys = false
|
|
757
|
-
): Extends<P2TR_MS, P2Ret
|
|
1293
|
+
): TRet<Extends<P2TR_MS, P2Ret>> {
|
|
758
1294
|
if (!allowSamePubkeys) uniqPubkey(pubkeys);
|
|
759
1295
|
return {
|
|
760
1296
|
type: 'tr_ms',
|
|
761
1297
|
script: OutScript.encode({ type: 'tr_ms', pubkeys, m }),
|
|
762
|
-
} as const
|
|
1298
|
+
} as const as TRet<Extends<P2TR_MS, P2Ret>>;
|
|
763
1299
|
}
|
|
764
1300
|
|
|
765
1301
|
// Simple pubkey address, without complex scripts
|
|
1302
|
+
/**
|
|
1303
|
+
* Derives a simple address from a private key.
|
|
1304
|
+
* @param type - address type to derive
|
|
1305
|
+
* @param privKey - private key bytes
|
|
1306
|
+
* @param network - address network parameters
|
|
1307
|
+
* @returns Encoded Bitcoin address.
|
|
1308
|
+
* @throws If the requested address type is unknown. {@link Error}
|
|
1309
|
+
* @example
|
|
1310
|
+
* Pick the output type first, then derive the matching address from the private key.
|
|
1311
|
+
* ```ts
|
|
1312
|
+
* import { getAddress } from '@scure/btc-signer/payment.js';
|
|
1313
|
+
* import { randomPrivateKeyBytes } from '@scure/btc-signer/utils.js';
|
|
1314
|
+
* getAddress('wpkh', randomPrivateKeyBytes());
|
|
1315
|
+
* ```
|
|
1316
|
+
*/
|
|
766
1317
|
export function getAddress(
|
|
767
1318
|
type: 'pkh' | 'wpkh' | 'tr',
|
|
768
|
-
privKey: Bytes
|
|
1319
|
+
privKey: TArg<Bytes>,
|
|
769
1320
|
network: BTC_NETWORK = NETWORK
|
|
770
|
-
): string
|
|
1321
|
+
): string {
|
|
1322
|
+
u.astring(type, 'type');
|
|
771
1323
|
if (type === 'tr') {
|
|
772
1324
|
return p2tr(u.pubSchnorr(privKey), undefined, network).address;
|
|
773
1325
|
}
|
|
1326
|
+
// This convenience wrapper always uses the compressed ECDSA public key; derive
|
|
1327
|
+
// `pubECDSA(privKey, false)` and call `p2pkh(...)` directly for legacy uncompressed P2PKH.
|
|
774
1328
|
const pubKey = u.pubECDSA(privKey);
|
|
775
1329
|
if (type === 'pkh') return p2pkh(pubKey, network).address;
|
|
776
1330
|
if (type === 'wpkh') return p2wpkh(pubKey, network).address;
|
|
777
1331
|
throw new Error(`getAddress: unknown type=${type}`);
|
|
778
1332
|
}
|
|
779
1333
|
|
|
780
|
-
|
|
1334
|
+
// BIP67 defines canonical multisig ordering only for compressed pubkeys; this helper still sorts
|
|
1335
|
+
// raw bytes generically, and higher-level callers may accept uncompressed participants for compat.
|
|
1336
|
+
export const _sortPubkeys = (pubkeys: TArg<Bytes[]>): TRet<Bytes[]> =>
|
|
1337
|
+
Array.from(pubkeys).sort(u.compareBytes) as TRet<Bytes[]>;
|
|
781
1338
|
|
|
1339
|
+
/**
|
|
1340
|
+
* Builds a classic M-of-N multisig output, wrapped in P2SH or P2WSH.
|
|
1341
|
+
* @param m - number of required signatures
|
|
1342
|
+
* @param pubkeys - participating public keys
|
|
1343
|
+
* @param sorted - whether to sort the public keys first
|
|
1344
|
+
* @param witness - whether to wrap the result as native SegWit
|
|
1345
|
+
* @param network - address network parameters
|
|
1346
|
+
* @returns Multisig payment descriptor.
|
|
1347
|
+
* @throws If the multisig parameters or wrapped script are invalid. {@link Error}
|
|
1348
|
+
* @example
|
|
1349
|
+
* Wrap a classic 2-of-2 script into an addressable multisig output.
|
|
1350
|
+
* ```ts
|
|
1351
|
+
* import { multisig } from '@scure/btc-signer/payment.js';
|
|
1352
|
+
* import { pubECDSA, randomPrivateKeyBytes } from '@scure/btc-signer/utils.js';
|
|
1353
|
+
* multisig(
|
|
1354
|
+
* 2,
|
|
1355
|
+
* [pubECDSA(randomPrivateKeyBytes()), pubECDSA(randomPrivateKeyBytes())],
|
|
1356
|
+
* true,
|
|
1357
|
+
* true
|
|
1358
|
+
* );
|
|
1359
|
+
* ```
|
|
1360
|
+
*/
|
|
782
1361
|
export function multisig(
|
|
783
1362
|
m: number,
|
|
784
|
-
pubkeys: Bytes[]
|
|
1363
|
+
pubkeys: TArg<Bytes[]>,
|
|
785
1364
|
sorted = false,
|
|
786
1365
|
witness = false,
|
|
787
1366
|
network: BTC_NETWORK = NETWORK
|
|
788
|
-
): P2Ret {
|
|
1367
|
+
): TRet<P2Ret> {
|
|
1368
|
+
// BIP 143 default policy: version-0 witness programs should use compressed ECDSA pubkeys only;
|
|
1369
|
+
// witness multisig callers must avoid uncompressed keys because p2ms accepts generic ECDSA encodings.
|
|
1370
|
+
// BIP 16 caps spendable compressed-key P2SH multisig at 15 pubkeys because larger redeem scripts
|
|
1371
|
+
// exceed the 520-byte push limit; use witness=true for larger classic multisig sets.
|
|
789
1372
|
const ms = p2ms(m, sorted ? _sortPubkeys(pubkeys) : pubkeys);
|
|
790
|
-
return witness ? p2wsh(ms, network) : p2sh(ms, network)
|
|
1373
|
+
return (witness ? p2wsh(ms, network) : p2sh(ms, network)) as TRet<P2Ret>;
|
|
791
1374
|
}
|
|
792
1375
|
|
|
1376
|
+
/**
|
|
1377
|
+
* Builds a multisig output after lexicographically sorting the keys.
|
|
1378
|
+
* @param m - number of required signatures
|
|
1379
|
+
* @param pubkeys - participating public keys
|
|
1380
|
+
* @param witness - whether to wrap the result as native SegWit
|
|
1381
|
+
* @param network - address network parameters
|
|
1382
|
+
* @returns Sorted multisig payment descriptor.
|
|
1383
|
+
* @throws If the multisig parameters or wrapped script are invalid. {@link Error}
|
|
1384
|
+
* @example
|
|
1385
|
+
* Sort public keys deterministically before constructing the multisig address.
|
|
1386
|
+
* ```ts
|
|
1387
|
+
* import { sortedMultisig } from '@scure/btc-signer/payment.js';
|
|
1388
|
+
* import { pubECDSA, randomPrivateKeyBytes } from '@scure/btc-signer/utils.js';
|
|
1389
|
+
* sortedMultisig(
|
|
1390
|
+
* 2,
|
|
1391
|
+
* [pubECDSA(randomPrivateKeyBytes()), pubECDSA(randomPrivateKeyBytes())],
|
|
1392
|
+
* true
|
|
1393
|
+
* );
|
|
1394
|
+
* ```
|
|
1395
|
+
*/
|
|
793
1396
|
export function sortedMultisig(
|
|
794
1397
|
m: number,
|
|
795
|
-
pubkeys: Bytes[]
|
|
1398
|
+
pubkeys: TArg<Bytes[]>,
|
|
796
1399
|
witness = false,
|
|
797
1400
|
network: BTC_NETWORK = NETWORK
|
|
798
|
-
): P2Ret {
|
|
799
|
-
|
|
1401
|
+
): TRet<P2Ret> {
|
|
1402
|
+
// BIP67 canonical multisig is compressed-only, but this wrapper intentionally keeps the generic
|
|
1403
|
+
// sorted-multisig behavior and still allows uncompressed participant keys for compatibility.
|
|
1404
|
+
return multisig(m, pubkeys, true, witness, network) as TRet<P2Ret>;
|
|
800
1405
|
}
|
|
801
1406
|
|
|
802
|
-
const base58check = createBase58check(u.sha256);
|
|
1407
|
+
const base58check = /* @__PURE__ */ createBase58check(u.sha256);
|
|
803
1408
|
|
|
804
|
-
function validateWitness(version: number, data: Bytes) {
|
|
1409
|
+
function validateWitness(version: number, data: TArg<Bytes>) {
|
|
805
1410
|
if (data.length < 2 || data.length > 40) throw new Error('Witness: invalid length');
|
|
806
1411
|
if (version > 16) throw new Error('Witness: invalid version');
|
|
807
1412
|
if (version === 0 && !(data.length === 20 || data.length === 32))
|
|
808
1413
|
throw new Error('Witness: invalid length for version');
|
|
809
1414
|
}
|
|
810
1415
|
|
|
811
|
-
function programToWitness(version: number, data: Bytes
|
|
1416
|
+
function programToWitness(version: number, data: TArg<Bytes>, network = NETWORK) {
|
|
812
1417
|
validateWitness(version, data);
|
|
1418
|
+
// BIP 350 keeps segwit v0 on Bech32, while witness versions 1+ switch to Bech32m.
|
|
813
1419
|
const coder = version === 0 ? bech32 : bech32m;
|
|
814
1420
|
return coder.encode(network.bech32, [version].concat(coder.toWords(data)));
|
|
815
1421
|
}
|
|
816
1422
|
|
|
817
|
-
function formatKey(hashed: Bytes
|
|
1423
|
+
function formatKey(hashed: TArg<Bytes>, prefix: number[]): string {
|
|
1424
|
+
// Legacy Base58Check paths all serialize [version-byte || payload] before checksumming.
|
|
818
1425
|
return base58check.encode(u.concatBytes(Uint8Array.from(prefix), hashed));
|
|
819
1426
|
}
|
|
820
1427
|
|
|
821
|
-
|
|
1428
|
+
/**
|
|
1429
|
+
* Wallet-import-format coder for private keys.
|
|
1430
|
+
* @param network - address network parameters
|
|
1431
|
+
* @returns WIF coder.
|
|
1432
|
+
* @example
|
|
1433
|
+
* Encode or decode wallet-import-format private keys.
|
|
1434
|
+
* ```ts
|
|
1435
|
+
* const coder = WIF();
|
|
1436
|
+
* coder.encode(new Uint8Array(32).fill(1));
|
|
1437
|
+
* ```
|
|
1438
|
+
*/
|
|
1439
|
+
export function WIF(network: BTC_NETWORK = NETWORK): TRet<Coder<Bytes, string>> {
|
|
822
1440
|
return {
|
|
823
|
-
encode(privKey: Bytes) {
|
|
1441
|
+
encode(privKey: TArg<Bytes>) {
|
|
1442
|
+
// Compressed WIF is exactly 32 private-key bytes plus the 0x01 suffix; shorter or longer
|
|
1443
|
+
// inputs must be rejected instead of being silently padded or truncated by subarray().
|
|
1444
|
+
abytes(privKey, 32, 'privKey');
|
|
824
1445
|
const compressed = u.concatBytes(privKey, new Uint8Array([0x01]));
|
|
825
1446
|
return formatKey(compressed.subarray(0, 33), [network.wif]);
|
|
826
1447
|
},
|
|
827
|
-
decode(wif: string) {
|
|
1448
|
+
decode(wif: string): TRet<Bytes> {
|
|
828
1449
|
let parsed = base58check.decode(wif);
|
|
829
1450
|
if (parsed[0] !== network.wif) throw new Error('Wrong WIF prefix');
|
|
830
1451
|
parsed = parsed.subarray(1);
|
|
831
1452
|
// Check what it is. Compressed flag?
|
|
832
1453
|
if (parsed.length !== 33) throw new Error('Wrong WIF length');
|
|
833
1454
|
if (parsed[32] !== 0x01) throw new Error('Wrong WIF postfix');
|
|
834
|
-
return parsed.subarray(0, -1)
|
|
1455
|
+
return parsed.subarray(0, -1) as TRet<Bytes>;
|
|
835
1456
|
},
|
|
836
1457
|
};
|
|
837
1458
|
}
|
|
838
1459
|
|
|
839
1460
|
// Returns OutType, which can be used to create outscript
|
|
840
|
-
|
|
1461
|
+
/**
|
|
1462
|
+
* Address encoder/decoder for a specific Bitcoin network.
|
|
1463
|
+
* @param network - address network parameters
|
|
1464
|
+
* @returns Address coder backed by the provided network.
|
|
1465
|
+
* @example
|
|
1466
|
+
* Create a network-specific address coder and encode a payment descriptor.
|
|
1467
|
+
* ```ts
|
|
1468
|
+
* import { Address, p2wpkh } from '@scure/btc-signer/payment.js';
|
|
1469
|
+
* import { pubECDSA, randomPrivateKeyBytes } from '@scure/btc-signer/utils.js';
|
|
1470
|
+
* const coder = Address();
|
|
1471
|
+
* coder.encode(p2wpkh(pubECDSA(randomPrivateKeyBytes())));
|
|
1472
|
+
* ```
|
|
1473
|
+
*/
|
|
1474
|
+
export function Address(network: BTC_NETWORK = NETWORK): TRet<P.Coder<AddressValue, string>> {
|
|
1475
|
+
u.validateObject(network as Record<string, any>, {}, {}, 'network');
|
|
841
1476
|
return {
|
|
842
|
-
encode(from:
|
|
1477
|
+
encode(from: TArg<AddressValue>): string {
|
|
1478
|
+
u.validateObject(from as Record<string, any>, {}, {}, 'from');
|
|
843
1479
|
const { type } = from;
|
|
1480
|
+
u.astring(type, 'from.type');
|
|
844
1481
|
if (type === 'wpkh') return programToWitness(0, from.hash, network);
|
|
845
1482
|
else if (type === 'wsh') return programToWitness(0, from.hash, network);
|
|
846
1483
|
else if (type === 'tr') return programToWitness(1, from.pubkey, network);
|
|
1484
|
+
// BIP433 P2A is the fixed v1 witness program 0x4e73 ('bc1pfeessrawgf').
|
|
1485
|
+
else if (type === 'p2a') return programToWitness(1, P2A_PROGRAM, network);
|
|
847
1486
|
else if (type === 'pkh') return formatKey(from.hash, [network.pubKeyHash]);
|
|
848
1487
|
else if (type === 'sh') return formatKey(from.hash, [network.scriptHash]);
|
|
849
1488
|
throw new Error(`Unknown address type=${type}`);
|
|
850
1489
|
},
|
|
851
|
-
decode(address: string):
|
|
1490
|
+
decode(address: string): TRet<AddressValue> {
|
|
1491
|
+
u.astring(address, 'address');
|
|
852
1492
|
if (address.length < 14 || address.length > 74) throw new Error('Invalid address length');
|
|
853
1493
|
// Bech32
|
|
854
1494
|
if (network.bech32 && address.toLowerCase().startsWith(`${network.bech32}1`)) {
|
|
@@ -865,21 +1505,28 @@ export function Address(network: BTC_NETWORK = NETWORK) {
|
|
|
865
1505
|
const [version, ...program] = res.words;
|
|
866
1506
|
const data = bech32.fromWords(program);
|
|
867
1507
|
validateWitness(version, data);
|
|
868
|
-
if (version === 0 && data.length === 32)
|
|
869
|
-
|
|
870
|
-
else if (version ===
|
|
1508
|
+
if (version === 0 && data.length === 32)
|
|
1509
|
+
return { type: 'wsh', hash: data } as TRet<AddressValue>;
|
|
1510
|
+
else if (version === 0 && data.length === 20)
|
|
1511
|
+
return { type: 'wpkh', hash: data } as TRet<AddressValue>;
|
|
1512
|
+
else if (version === 1 && data.length === 32)
|
|
1513
|
+
return { type: 'tr', pubkey: data } as TRet<AddressValue>;
|
|
1514
|
+
else if (version === 1 && u.equalBytes(data, P2A_PROGRAM))
|
|
1515
|
+
return { type: 'p2a', script: Script.encode([1, data]) } as TRet<AddressValue>;
|
|
1516
|
+
// Future witness versions can still be valid addresses, but this helper
|
|
1517
|
+
// only returns typed descriptors for recognized v0, taproot and P2A templates.
|
|
871
1518
|
else throw new Error('Unknown witness program');
|
|
872
1519
|
}
|
|
873
1520
|
const data = base58check.decode(address);
|
|
874
1521
|
if (data.length !== 21) throw new Error('Invalid base58 address');
|
|
875
1522
|
// Pay To Public Key Hash
|
|
876
1523
|
if (data[0] === network.pubKeyHash) {
|
|
877
|
-
return { type: 'pkh', hash: data.slice(1) }
|
|
1524
|
+
return { type: 'pkh', hash: data.slice(1) } as TRet<AddressValue>;
|
|
878
1525
|
} else if (data[0] === network.scriptHash) {
|
|
879
1526
|
return {
|
|
880
1527
|
type: 'sh',
|
|
881
1528
|
hash: data.slice(1),
|
|
882
|
-
}
|
|
1529
|
+
} as TRet<AddressValue>;
|
|
883
1530
|
}
|
|
884
1531
|
throw new Error(`Invalid address prefix=${data[0]}`);
|
|
885
1532
|
},
|