@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/README.md +106 -48
- package/lib/Btc.d.ts.map +1 -1
- package/lib/Btc.js +4 -2
- package/lib/Btc.js.map +1 -1
- package/lib/BtcNew.d.ts.map +1 -1
- package/lib/BtcNew.js +35 -164
- package/lib/BtcNew.js.map +1 -1
- package/lib/newops/accounttype.d.ts +110 -0
- package/lib/newops/accounttype.d.ts.map +1 -0
- package/lib/newops/accounttype.js +233 -0
- package/lib/newops/accounttype.js.map +1 -0
- package/lib/newops/clientCommands.d.ts.map +1 -1
- package/lib/newops/clientCommands.js +14 -9
- package/lib/newops/clientCommands.js.map +1 -1
- package/lib-es/Btc.d.ts.map +1 -1
- package/lib-es/Btc.js +4 -2
- package/lib-es/Btc.js.map +1 -1
- package/lib-es/BtcNew.d.ts.map +1 -1
- package/lib-es/BtcNew.js +38 -167
- package/lib-es/BtcNew.js.map +1 -1
- package/lib-es/newops/accounttype.d.ts +110 -0
- package/lib-es/newops/accounttype.d.ts.map +1 -0
- package/lib-es/newops/accounttype.js +230 -0
- package/lib-es/newops/accounttype.js.map +1 -0
- package/lib-es/newops/clientCommands.d.ts.map +1 -1
- package/lib-es/newops/clientCommands.js +14 -9
- package/lib-es/newops/clientCommands.js.map +1 -1
- package/package.json +3 -3
- package/src/Btc.ts +33 -2
- package/src/BtcNew.ts +61 -163
- package/src/newops/accounttype.ts +370 -0
- package/src/newops/clientCommands.ts +15 -9
- package/tests/Btc.test.ts +5 -0
- package/tests/newops/BtcNew.test.ts +10 -10
- package/tests/newops/integrationtools.ts +17 -17
package/src/BtcNew.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { crypto } from "bitcoinjs-lib";
|
|
2
2
|
import semver from "semver";
|
|
3
|
-
import {
|
|
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
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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?.
|
|
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
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
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(
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
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
|
+
}
|