@ledgerhq/hw-app-btc 6.11.1 → 6.11.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/src/BtcNew.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { crypto } from "bitcoinjs-lib";
2
2
  import semver from "semver";
3
- import { pointAddScalar, pointCompress } from "tiny-secp256k1";
3
+ import { pointCompress } from "tiny-secp256k1";
4
4
  import {
5
5
  getXpubComponents,
6
6
  hardenedPathOf,
@@ -8,21 +8,24 @@ import {
8
8
  pathStringToArray,
9
9
  pubkeyFromXpub,
10
10
  } from "./bip32";
11
- import { BufferReader, BufferWriter } from "./buffertools";
12
- import {
13
- HASH_SIZE,
14
- OP_CHECKSIG,
15
- OP_DUP,
16
- OP_EQUAL,
17
- OP_EQUALVERIFY,
18
- OP_HASH160,
19
- } from "./constants";
11
+ import { BufferReader } from "./buffertools";
20
12
  import type { CreateTransactionArg } from "./createTransaction";
21
13
  import { AppAndVersion } from "./getAppAndVersion";
22
14
  import type { AddressFormat } from "./getWalletPublicKey";
23
- import { hashPublicKey } from "./hashPublicKey";
15
+ import {
16
+ AccountType,
17
+ p2pkh,
18
+ p2tr,
19
+ p2wpkh,
20
+ p2wpkhWrapped,
21
+ SpendingCondition,
22
+ } from "./newops/accounttype";
24
23
  import { AppClient as Client } from "./newops/appClient";
25
- import { createKey, WalletPolicy } from "./newops/policy";
24
+ import {
25
+ createKey,
26
+ DefaultDescriptorTemplate,
27
+ WalletPolicy,
28
+ } from "./newops/policy";
26
29
  import { extract } from "./newops/psbtExtractor";
27
30
  import { finalize } from "./newops/psbtFinalizer";
28
31
  import { psbtIn, PsbtV2 } from "./newops/psbtv2";
@@ -127,7 +130,7 @@ export default class BtcNew {
127
130
 
128
131
  const address = await this.getWalletAddress(
129
132
  pathElements,
130
- accountTypeFrom(opts?.format ?? "legacy"),
133
+ descrTemplFrom(opts?.format ?? "legacy"),
131
134
  display
132
135
  );
133
136
  const components = getXpubComponents(xpub);
@@ -158,7 +161,7 @@ export default class BtcNew {
158
161
  */
159
162
  private async getWalletAddress(
160
163
  pathElements: number[],
161
- accountType: AccountType,
164
+ descrTempl: DefaultDescriptorTemplate,
162
165
  display: boolean
163
166
  ): Promise<string> {
164
167
  const accountPath = hardenedPathOf(pathElements);
@@ -168,7 +171,7 @@ export default class BtcNew {
168
171
  const accountXpub = await this.client.getExtendedPubkey(false, accountPath);
169
172
  const masterFingerprint = await this.client.getMasterFingerprint();
170
173
  const policy = new WalletPolicy(
171
- accountType,
174
+ descrTempl,
172
175
  createKey(masterFingerprint, accountPath, accountXpub)
173
176
  );
174
177
  const changeAndIndex = pathElements.slice(-2, pathElements.length);
@@ -197,8 +200,11 @@ export default class BtcNew {
197
200
  throw Error("No inputs");
198
201
  }
199
202
  const psbt = new PsbtV2();
203
+ // The master fingerprint is needed when adding BIP32 derivation paths on
204
+ // the psbt.
205
+ const masterFp = await this.client.getMasterFingerprint();
200
206
 
201
- const accountType = accountTypeFromArg(arg);
207
+ const accountType = accountTypeFromArg(arg, psbt, masterFp);
202
208
 
203
209
  if (arg.lockTime) {
204
210
  // The signer will assume locktime 0 if unset
@@ -218,9 +224,6 @@ export default class BtcNew {
218
224
  });
219
225
  };
220
226
 
221
- // The master fingerprint is needed when adding BIP32 derivation paths on
222
- // the psbt.
223
- const masterFp = await this.client.getMasterFingerprint();
224
227
  let accountXpub = "";
225
228
  let accountPath: number[] = [];
226
229
  for (let i = 0; i < inputCount; i++) {
@@ -264,35 +267,26 @@ export default class BtcNew {
264
267
  // We won't know if we're paying to ourselves, because there's no
265
268
  // information in arg to support multiple "change paths". One exception is
266
269
  // if there are multiple outputs to the change address.
267
- const isChange = changeData && outputScript.equals(changeData?.script);
270
+ const isChange =
271
+ changeData && outputScript.equals(changeData?.cond.scriptPubKey);
268
272
  if (isChange) {
269
273
  changeFound = true;
270
274
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
271
275
  const changePath = pathStringToArray(arg.changePath!);
272
276
  const pubkey = changeData.pubkey;
273
277
 
274
- if (accountType == AccountType.p2pkh) {
275
- psbt.setOutputBip32Derivation(i, pubkey, masterFp, changePath);
276
- } else if (accountType == AccountType.p2wpkh) {
277
- psbt.setOutputBip32Derivation(i, pubkey, masterFp, changePath);
278
- } else if (accountType == AccountType.p2wpkhWrapped) {
279
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
280
- psbt.setOutputRedeemScript(i, changeData.redeemScript!);
281
- psbt.setOutputBip32Derivation(i, pubkey, masterFp, changePath);
282
- } else if (accountType == AccountType.p2tr) {
283
- psbt.setOutputTapBip32Derivation(i, pubkey, [], masterFp, changePath);
284
- }
278
+ accountType.setOwnOutput(i, changeData.cond, [pubkey], [changePath]);
285
279
  }
286
280
  }
287
281
  if (!changeFound) {
288
282
  throw new Error(
289
283
  "Change script not found among outputs! " +
290
- changeData?.script.toString("hex")
284
+ changeData?.cond.scriptPubKey.toString("hex")
291
285
  );
292
286
  }
293
287
 
294
288
  const key = createKey(masterFp, accountPath, accountXpub);
295
- const p = new WalletPolicy(accountType, key);
289
+ const p = new WalletPolicy(accountType.getDescriptorTemplate(), key);
296
290
  // This is cheating, because it's not actually requested on the
297
291
  // device yet, but it will be, soonish.
298
292
  if (arg.onDeviceSignatureRequested) arg.onDeviceSignatureRequested();
@@ -325,9 +319,7 @@ export default class BtcNew {
325
319
  accountPath: number[],
326
320
  accountType: AccountType,
327
321
  path: string | undefined
328
- ): Promise<
329
- { script: Buffer; redeemScript?: Buffer; pubkey: Buffer } | undefined
330
- > {
322
+ ): Promise<{ cond: SpendingCondition; pubkey: Buffer } | undefined> {
331
323
  if (!path) return undefined;
332
324
  const pathElems = pathStringToArray(path);
333
325
  // Make sure path is in our account, otherwise something fishy is probably
@@ -340,12 +332,9 @@ export default class BtcNew {
340
332
  }
341
333
  }
342
334
  const xpub = await this.client.getExtendedPubkey(false, pathElems);
343
- let pubkey = pubkeyFromXpub(xpub);
344
- if (accountType == AccountType.p2tr) {
345
- pubkey = pubkey.slice(1);
346
- }
347
- const script = outputScriptOf(pubkey, accountType);
348
- return { ...script, pubkey };
335
+ const pubkey = pubkeyFromXpub(xpub);
336
+ const cond = accountType.spendingCondition([pubkey]);
337
+ return { cond, pubkey };
349
338
  }
350
339
 
351
340
  /**
@@ -371,7 +360,7 @@ export default class BtcNew {
371
360
  const spentOutputIndex = input[1];
372
361
  // redeemScript will be null for wrapped p2wpkh, we need to create it
373
362
  // ourselves. But if set, it should be used.
374
- const redeemScript = input[2];
363
+ const redeemScript = input[2] ? Buffer.from(input[2], "hex") : undefined;
375
364
  const sequence = input[3];
376
365
  if (sequence) {
377
366
  psbt.setInputSequence(i, sequence);
@@ -386,30 +375,19 @@ export default class BtcNew {
386
375
  const pubkey = pubkeyFromXpub(xpubBase58);
387
376
  if (!inputTx.outputs)
388
377
  throw Error("Missing outputs array in transaction to sign");
389
- const spentOutput = inputTx.outputs[spentOutputIndex];
390
-
391
- if (accountType == AccountType.p2pkh) {
392
- psbt.setInputNonWitnessUtxo(i, inputTxBuffer);
393
- psbt.setInputBip32Derivation(i, pubkey, masterFP, pathElements);
394
- } else if (accountType == AccountType.p2wpkh) {
395
- psbt.setInputNonWitnessUtxo(i, inputTxBuffer);
396
- psbt.setInputBip32Derivation(i, pubkey, masterFP, pathElements);
397
- psbt.setInputWitnessUtxo(i, spentOutput.amount, spentOutput.script);
398
- } else if (accountType == AccountType.p2wpkhWrapped) {
399
- psbt.setInputNonWitnessUtxo(i, inputTxBuffer);
400
- psbt.setInputBip32Derivation(i, pubkey, masterFP, pathElements);
401
- if (redeemScript) {
402
- // At what point might a user set the redeemScript on its own?
403
- psbt.setInputRedeemScript(i, Buffer.from(redeemScript, "hex"));
404
- } else {
405
- psbt.setInputRedeemScript(i, createRedeemScript(pubkey));
406
- }
407
- psbt.setInputWitnessUtxo(i, spentOutput.amount, spentOutput.script);
408
- } else if (accountType == AccountType.p2tr) {
409
- const xonly = pubkey.slice(1);
410
- psbt.setInputTapBip32Derivation(i, xonly, [], masterFP, pathElements);
411
- psbt.setInputWitnessUtxo(i, spentOutput.amount, spentOutput.script);
412
- }
378
+ const spentTxOutput = inputTx.outputs[spentOutputIndex];
379
+ const spendCondition: SpendingCondition = {
380
+ scriptPubKey: spentTxOutput.script,
381
+ redeemScript: redeemScript,
382
+ };
383
+ const spentOutput = { cond: spendCondition, amount: spentTxOutput.amount };
384
+ accountType.setInput(
385
+ i,
386
+ inputTxBuffer,
387
+ spentOutput,
388
+ [pubkey],
389
+ [pathElements]
390
+ );
413
391
 
414
392
  psbt.setInputPreviousTxId(i, inputTxid);
415
393
  psbt.setInputOutputIndex(i, spentOutputIndex);
@@ -455,103 +433,23 @@ export default class BtcNew {
455
433
  }
456
434
  }
457
435
 
458
- enum AccountType {
459
- p2pkh = "pkh(@0)",
460
- p2wpkh = "wpkh(@0)",
461
- p2wpkhWrapped = "sh(wpkh(@0))",
462
- p2tr = "tr(@0)",
463
- }
464
-
465
- function createRedeemScript(pubkey: Buffer): Buffer {
466
- const pubkeyHash = hashPublicKey(pubkey);
467
- return Buffer.concat([Buffer.from("0014", "hex"), pubkeyHash]);
468
- }
469
-
470
- /**
471
- * Generates a single signature scriptPubKey (output script) from a public key.
472
- * This is done differently depending on account type.
473
- *
474
- * If accountType is p2tr, the public key must be a 32 byte x-only taproot
475
- * pubkey, otherwise it's expected to be a 33 byte ecdsa compressed pubkey.
476
- */
477
- function outputScriptOf(
478
- pubkey: Buffer,
479
- accountType: AccountType
480
- ): { script: Buffer; redeemScript?: Buffer } {
481
- const buf = new BufferWriter();
482
- const pubkeyHash = hashPublicKey(pubkey);
483
- let redeemScript: Buffer | undefined;
484
- if (accountType == AccountType.p2pkh) {
485
- buf.writeSlice(Buffer.of(OP_DUP, OP_HASH160, HASH_SIZE));
486
- buf.writeSlice(pubkeyHash);
487
- buf.writeSlice(Buffer.of(OP_EQUALVERIFY, OP_CHECKSIG));
488
- } else if (accountType == AccountType.p2wpkhWrapped) {
489
- redeemScript = createRedeemScript(pubkey);
490
- const scriptHash = hashPublicKey(redeemScript);
491
- buf.writeSlice(Buffer.of(OP_HASH160, HASH_SIZE));
492
- buf.writeSlice(scriptHash);
493
- buf.writeUInt8(OP_EQUAL);
494
- } else if (accountType == AccountType.p2wpkh) {
495
- buf.writeSlice(Buffer.of(0, HASH_SIZE));
496
- buf.writeSlice(pubkeyHash);
497
- } else if (accountType == AccountType.p2tr) {
498
- const outputKey = getTaprootOutputKey(pubkey);
499
- buf.writeSlice(Buffer.of(0x51, 32)); // push1, pubkeylen
500
- buf.writeSlice(outputKey);
501
- }
502
- return { script: buf.buffer(), redeemScript };
503
- }
504
-
505
- function accountTypeFrom(addressFormat: AddressFormat): AccountType {
506
- if (addressFormat == "legacy") return AccountType.p2pkh;
507
- if (addressFormat == "p2sh") return AccountType.p2wpkhWrapped;
508
- if (addressFormat == "bech32") return AccountType.p2wpkh;
509
- if (addressFormat == "bech32m") return AccountType.p2tr;
436
+ function descrTemplFrom(
437
+ addressFormat: AddressFormat
438
+ ): DefaultDescriptorTemplate {
439
+ if (addressFormat == "legacy") return "pkh(@0)";
440
+ if (addressFormat == "p2sh") return "sh(wpkh(@0))";
441
+ if (addressFormat == "bech32") return "wpkh(@0)";
442
+ if (addressFormat == "bech32m") return "tr(@0)";
510
443
  throw new Error("Unsupported address format " + addressFormat);
511
444
  }
512
445
 
513
- function accountTypeFromArg(arg: CreateTransactionArg): AccountType {
514
- if (arg.additionals.includes("bech32m")) return AccountType.p2tr;
515
- if (arg.additionals.includes("bech32")) return AccountType.p2wpkh;
516
- if (arg.segwit) return AccountType.p2wpkhWrapped;
517
- return AccountType.p2pkh;
518
- }
519
-
520
- /*
521
- The following two functions are copied from wallet-btc and adapted.
522
- They should be moved to a library to avoid code reuse.
523
- */
524
- function hashTapTweak(x: Buffer): Buffer {
525
- // hash_tag(x) = SHA256(SHA256(tag) || SHA256(tag) || x), see BIP340
526
- // See https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki#specification
527
- const h = crypto.sha256(Buffer.from("TapTweak", "utf-8"));
528
- return crypto.sha256(Buffer.concat([h, h, x]));
529
- }
530
-
531
- /**
532
- * Calculates a taproot output key from an internal key. This output key will be
533
- * used as witness program in a taproot output. The internal key is tweaked
534
- * according to recommendation in BIP341:
535
- * https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#cite_ref-22-0
536
- *
537
- * @param internalPubkey A 32 byte x-only taproot internal key
538
- * @returns The output key
539
- */
540
- function getTaprootOutputKey(internalPubkey: Buffer): Buffer {
541
- if (internalPubkey.length != 32) {
542
- throw new Error("Expected 32 byte pubkey. Got " + internalPubkey.length);
543
- }
544
- // A BIP32 derived key can be converted to a schnorr pubkey by dropping
545
- // the first byte, which represent the oddness/evenness. In schnorr all
546
- // pubkeys are even.
547
- // https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki#public-key-conversion
548
- const evenEcdsaPubkey = Buffer.concat([Buffer.of(0x02), internalPubkey]);
549
- const tweak = hashTapTweak(internalPubkey);
550
-
551
- // Q = P + int(hash_TapTweak(bytes(P)))G
552
- const outputEcdsaKey = Buffer.from(pointAddScalar(evenEcdsaPubkey, tweak));
553
- // Convert to schnorr.
554
- const outputSchnorrKey = outputEcdsaKey.slice(1);
555
- // Create address
556
- return outputSchnorrKey;
446
+ function accountTypeFromArg(
447
+ arg: CreateTransactionArg,
448
+ psbt: PsbtV2,
449
+ masterFp: Buffer
450
+ ): AccountType {
451
+ if (arg.additionals.includes("bech32m")) return new p2tr(psbt, masterFp);
452
+ if (arg.additionals.includes("bech32")) return new p2wpkh(psbt, masterFp);
453
+ if (arg.segwit) return new p2wpkhWrapped(psbt, masterFp);
454
+ return new p2pkh(psbt, masterFp);
557
455
  }
@@ -0,0 +1,370 @@
1
+ import { crypto } from "bitcoinjs-lib";
2
+ import { pointAddScalar } from "tiny-secp256k1";
3
+ import { BufferWriter } from "../buffertools";
4
+ import {
5
+ HASH_SIZE,
6
+ OP_CHECKSIG,
7
+ OP_DUP,
8
+ OP_EQUAL,
9
+ OP_EQUALVERIFY,
10
+ OP_HASH160,
11
+ } from "../constants";
12
+ import { hashPublicKey } from "../hashPublicKey";
13
+ import { DefaultDescriptorTemplate } from "./policy";
14
+ import { PsbtV2 } from "./psbtv2";
15
+
16
+ export type SpendingCondition = {
17
+ scriptPubKey: Buffer;
18
+ redeemScript?: Buffer;
19
+ // Possible future extension:
20
+ // witnessScript?: Buffer; // For p2wsh witnessScript
21
+ // tapScript?: {tapPath: Buffer[], script: Buffer} // For taproot
22
+ };
23
+
24
+ export type SpentOutput = { cond: SpendingCondition; amount: Buffer };
25
+
26
+ /**
27
+ * Encapsulates differences between account types, for example p2wpkh,
28
+ * p2wpkhWrapped, p2tr.
29
+ */
30
+ export interface AccountType {
31
+ /**
32
+ * Generates a scriptPubKey (output script) from a list of public keys. If a
33
+ * p2sh redeemScript or a p2wsh witnessScript is needed it will also be set on
34
+ * the returned SpendingCondition.
35
+ *
36
+ * The pubkeys are expected to be 33 byte ecdsa compressed pubkeys.
37
+ */
38
+ spendingCondition(pubkeys: Buffer[]): SpendingCondition;
39
+
40
+ /**
41
+ * Populates the psbt with account type-specific data for an input.
42
+ * @param i The index of the input map to populate
43
+ * @param inputTx The full transaction containing the spent output. This may
44
+ * be omitted for taproot.
45
+ * @param spentOutput The amount and spending condition of the spent output
46
+ * @param pubkeys The 33 byte ecdsa compressed public keys involved in the input
47
+ * @param pathElems The paths corresponding to the pubkeys, in same order.
48
+ */
49
+ setInput(
50
+ i: number,
51
+ inputTx: Buffer | undefined,
52
+ spentOutput: SpentOutput,
53
+ pubkeys: Buffer[],
54
+ pathElems: number[][]
55
+ ): void;
56
+
57
+ /**
58
+ * Populates the psbt with account type-specific data for an output. This is typically
59
+ * done for change outputs and other outputs that goes to the same account as
60
+ * being spent from.
61
+ * @param i The index of the output map to populate
62
+ * @param cond The spending condition for this output
63
+ * @param pubkeys The 33 byte ecdsa compressed public keys involved in this output
64
+ * @param paths The paths corresponding to the pubkeys, in same order.
65
+ */
66
+ setOwnOutput(
67
+ i: number,
68
+ cond: SpendingCondition,
69
+ pubkeys: Buffer[],
70
+ paths: number[][]
71
+ ): void;
72
+
73
+ /**
74
+ * Returns the descriptor template for this account type. Currently only
75
+ * DefaultDescriptorTemplates are allowed, but that might be changed in the
76
+ * future. See class WalletPolicy for more information on descriptor
77
+ * templates.
78
+ */
79
+ getDescriptorTemplate(): DefaultDescriptorTemplate;
80
+ }
81
+
82
+ // eslint-disable-next-line @typescript-eslint/no-empty-interface
83
+ interface BaseAccount extends AccountType {}
84
+
85
+ abstract class BaseAccount implements AccountType {
86
+ constructor(protected psbt: PsbtV2, protected masterFp: Buffer) {}
87
+ }
88
+
89
+ /**
90
+ * Superclass for single signature accounts. This will make sure that the pubkey
91
+ * arrays and path arrays in the method arguments contains exactly one element
92
+ * and calls an abstract method to do the actual work.
93
+ */
94
+ abstract class SingleKeyAccount extends BaseAccount {
95
+ spendingCondition(pubkeys: Buffer[]): SpendingCondition {
96
+ if (pubkeys.length != 1) {
97
+ throw new Error("Expected single key, got " + pubkeys.length);
98
+ }
99
+ return this.singleKeyCondition(pubkeys[0]);
100
+ }
101
+ protected abstract singleKeyCondition(pubkey: Buffer): SpendingCondition;
102
+
103
+ setInput(
104
+ i: number,
105
+ inputTx: Buffer | undefined,
106
+ spentOutput: SpentOutput,
107
+ pubkeys: Buffer[],
108
+ pathElems: number[][]
109
+ ) {
110
+ if (pubkeys.length != 1) {
111
+ throw new Error("Expected single key, got " + pubkeys.length);
112
+ }
113
+ if (pathElems.length != 1) {
114
+ throw new Error("Expected single path, got " + pathElems.length);
115
+ }
116
+ this.setSingleKeyInput(i, inputTx, spentOutput, pubkeys[0], pathElems[0]);
117
+ }
118
+ protected abstract setSingleKeyInput(
119
+ i: number,
120
+ inputTx: Buffer | undefined,
121
+ spentOutput: SpentOutput,
122
+ pubkey: Buffer,
123
+ path: number[]
124
+ );
125
+
126
+ setOwnOutput(
127
+ i: number,
128
+ cond: SpendingCondition,
129
+ pubkeys: Buffer[],
130
+ paths: number[][]
131
+ ) {
132
+ if (pubkeys.length != 1) {
133
+ throw new Error("Expected single key, got " + pubkeys.length);
134
+ }
135
+ if (paths.length != 1) {
136
+ throw new Error("Expected single path, got " + paths.length);
137
+ }
138
+ this.setSingleKeyOutput(i, cond, pubkeys[0], paths[0]);
139
+ }
140
+ protected abstract setSingleKeyOutput(
141
+ i: number,
142
+ cond: SpendingCondition,
143
+ pubkey: Buffer,
144
+ path: number[]
145
+ );
146
+ }
147
+
148
+ export class p2pkh extends SingleKeyAccount {
149
+ singleKeyCondition(pubkey: Buffer): SpendingCondition {
150
+ const buf = new BufferWriter();
151
+ const pubkeyHash = hashPublicKey(pubkey);
152
+ buf.writeSlice(Buffer.of(OP_DUP, OP_HASH160, HASH_SIZE));
153
+ buf.writeSlice(pubkeyHash);
154
+ buf.writeSlice(Buffer.of(OP_EQUALVERIFY, OP_CHECKSIG));
155
+ return { scriptPubKey: buf.buffer() };
156
+ }
157
+
158
+ setSingleKeyInput(
159
+ i: number,
160
+ inputTx: Buffer | undefined,
161
+ _spentOutput: SpentOutput,
162
+ pubkey: Buffer,
163
+ path: number[]
164
+ ) {
165
+ if (!inputTx) {
166
+ throw new Error("Full input base transaction required");
167
+ }
168
+ this.psbt.setInputNonWitnessUtxo(i, inputTx);
169
+ this.psbt.setInputBip32Derivation(i, pubkey, this.masterFp, path);
170
+ }
171
+
172
+ setSingleKeyOutput(
173
+ i: number,
174
+ cond: SpendingCondition,
175
+ pubkey: Buffer,
176
+ path: number[]
177
+ ) {
178
+ this.psbt.setOutputBip32Derivation(i, pubkey, this.masterFp, path);
179
+ }
180
+
181
+ getDescriptorTemplate(): DefaultDescriptorTemplate {
182
+ return "pkh(@0)";
183
+ }
184
+ }
185
+
186
+ export class p2tr extends SingleKeyAccount {
187
+ singleKeyCondition(pubkey: Buffer): SpendingCondition {
188
+ const xonlyPubkey = pubkey.slice(1); // x-only pubkey
189
+ const buf = new BufferWriter();
190
+ const outputKey = this.getTaprootOutputKey(xonlyPubkey);
191
+ buf.writeSlice(Buffer.of(0x51, 32)); // push1, pubkeylen
192
+ buf.writeSlice(outputKey);
193
+ return { scriptPubKey: buf.buffer() };
194
+ }
195
+
196
+ setSingleKeyInput(
197
+ i: number,
198
+ _inputTx: Buffer | undefined,
199
+ spentOutput: SpentOutput,
200
+ pubkey: Buffer,
201
+ path: number[]
202
+ ) {
203
+ const xonly = pubkey.slice(1);
204
+ this.psbt.setInputTapBip32Derivation(i, xonly, [], this.masterFp, path);
205
+ this.psbt.setInputWitnessUtxo(
206
+ i,
207
+ spentOutput.amount,
208
+ spentOutput.cond.scriptPubKey
209
+ );
210
+ }
211
+
212
+ setSingleKeyOutput(
213
+ i: number,
214
+ cond: SpendingCondition,
215
+ pubkey: Buffer,
216
+ path: number[]
217
+ ) {
218
+ const xonly = pubkey.slice(1);
219
+ this.psbt.setOutputTapBip32Derivation(i, xonly, [], this.masterFp, path);
220
+ }
221
+
222
+ getDescriptorTemplate(): DefaultDescriptorTemplate {
223
+ return "tr(@0)";
224
+ }
225
+
226
+ /*
227
+ The following two functions are copied from wallet-btc and adapted.
228
+ They should be moved to a library to avoid code reuse.
229
+ */
230
+ private hashTapTweak(x: Buffer): Buffer {
231
+ // hash_tag(x) = SHA256(SHA256(tag) || SHA256(tag) || x), see BIP340
232
+ // See https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki#specification
233
+ const h = crypto.sha256(Buffer.from("TapTweak", "utf-8"));
234
+ return crypto.sha256(Buffer.concat([h, h, x]));
235
+ }
236
+
237
+ /**
238
+ * Calculates a taproot output key from an internal key. This output key will be
239
+ * used as witness program in a taproot output. The internal key is tweaked
240
+ * according to recommendation in BIP341:
241
+ * https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#cite_ref-22-0
242
+ *
243
+ * @param internalPubkey A 32 byte x-only taproot internal key
244
+ * @returns The output key
245
+ */
246
+ getTaprootOutputKey(internalPubkey: Buffer): Buffer {
247
+ if (internalPubkey.length != 32) {
248
+ throw new Error("Expected 32 byte pubkey. Got " + internalPubkey.length);
249
+ }
250
+ // A BIP32 derived key can be converted to a schnorr pubkey by dropping
251
+ // the first byte, which represent the oddness/evenness. In schnorr all
252
+ // pubkeys are even.
253
+ // https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki#public-key-conversion
254
+ const evenEcdsaPubkey = Buffer.concat([Buffer.of(0x02), internalPubkey]);
255
+ const tweak = this.hashTapTweak(internalPubkey);
256
+
257
+ // Q = P + int(hash_TapTweak(bytes(P)))G
258
+ const outputEcdsaKey = Buffer.from(pointAddScalar(evenEcdsaPubkey, tweak));
259
+ // Convert to schnorr.
260
+ const outputSchnorrKey = outputEcdsaKey.slice(1);
261
+ // Create address
262
+ return outputSchnorrKey;
263
+ }
264
+ }
265
+
266
+ export class p2wpkhWrapped extends SingleKeyAccount {
267
+ singleKeyCondition(pubkey: Buffer): SpendingCondition {
268
+ const buf = new BufferWriter();
269
+ const redeemScript = this.createRedeemScript(pubkey);
270
+ const scriptHash = hashPublicKey(redeemScript);
271
+ buf.writeSlice(Buffer.of(OP_HASH160, HASH_SIZE));
272
+ buf.writeSlice(scriptHash);
273
+ buf.writeUInt8(OP_EQUAL);
274
+ return { scriptPubKey: buf.buffer(), redeemScript: redeemScript };
275
+ }
276
+
277
+ setSingleKeyInput(
278
+ i: number,
279
+ inputTx: Buffer | undefined,
280
+ spentOutput: SpentOutput,
281
+ pubkey: Buffer,
282
+ path: number[]
283
+ ) {
284
+ if (!inputTx) {
285
+ throw new Error("Full input base transaction required");
286
+ }
287
+ this.psbt.setInputNonWitnessUtxo(i, inputTx);
288
+ this.psbt.setInputBip32Derivation(i, pubkey, this.masterFp, path);
289
+
290
+ const userSuppliedRedeemScript = spentOutput.cond.redeemScript;
291
+ const expectedRedeemScript = this.createRedeemScript(pubkey);
292
+ if (
293
+ userSuppliedRedeemScript &&
294
+ !expectedRedeemScript.equals(userSuppliedRedeemScript)
295
+ ) {
296
+ // At what point might a user set the redeemScript on its own?
297
+ throw new Error(`User-supplied redeemScript ${userSuppliedRedeemScript.toString(
298
+ "hex"
299
+ )} doesn't
300
+ match expected ${expectedRedeemScript.toString("hex")} for input ${i}`);
301
+ }
302
+ this.psbt.setInputRedeemScript(i, expectedRedeemScript);
303
+ this.psbt.setInputWitnessUtxo(
304
+ i,
305
+ spentOutput.amount,
306
+ spentOutput.cond.scriptPubKey
307
+ );
308
+ }
309
+
310
+ setSingleKeyOutput(
311
+ i: number,
312
+ cond: SpendingCondition,
313
+ pubkey: Buffer,
314
+ path: number[]
315
+ ) {
316
+ this.psbt.setOutputRedeemScript(i, cond.redeemScript!);
317
+ this.psbt.setOutputBip32Derivation(i, pubkey, this.masterFp, path);
318
+ }
319
+
320
+ getDescriptorTemplate(): DefaultDescriptorTemplate {
321
+ return "sh(wpkh(@0))";
322
+ }
323
+
324
+ private createRedeemScript(pubkey: Buffer): Buffer {
325
+ const pubkeyHash = hashPublicKey(pubkey);
326
+ return Buffer.concat([Buffer.from("0014", "hex"), pubkeyHash]);
327
+ }
328
+ }
329
+
330
+ export class p2wpkh extends SingleKeyAccount {
331
+ singleKeyCondition(pubkey: Buffer): SpendingCondition {
332
+ const buf = new BufferWriter();
333
+ const pubkeyHash = hashPublicKey(pubkey);
334
+ buf.writeSlice(Buffer.of(0, HASH_SIZE));
335
+ buf.writeSlice(pubkeyHash);
336
+ return { scriptPubKey: buf.buffer() };
337
+ }
338
+
339
+ setSingleKeyInput(
340
+ i: number,
341
+ inputTx: Buffer | undefined,
342
+ spentOutput: SpentOutput,
343
+ pubkey: Buffer,
344
+ path: number[]
345
+ ) {
346
+ if (!inputTx) {
347
+ throw new Error("Full input base transaction required");
348
+ }
349
+ this.psbt.setInputNonWitnessUtxo(i, inputTx);
350
+ this.psbt.setInputBip32Derivation(i, pubkey, this.masterFp, path);
351
+ this.psbt.setInputWitnessUtxo(
352
+ i,
353
+ spentOutput.amount,
354
+ spentOutput.cond.scriptPubKey
355
+ );
356
+ }
357
+
358
+ setSingleKeyOutput(
359
+ i: number,
360
+ cond: SpendingCondition,
361
+ pubkey: Buffer,
362
+ path: number[]
363
+ ) {
364
+ this.psbt.setOutputBip32Derivation(i, pubkey, this.masterFp, path);
365
+ }
366
+
367
+ getDescriptorTemplate(): DefaultDescriptorTemplate {
368
+ return "wpkh(@0)";
369
+ }
370
+ }