@ledgerhq/hw-app-btc 10.16.0-nightly.20260115024415 → 10.16.0-nightly.20260116124336
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/CHANGELOG.md +5 -3
- package/README.md +106 -56
- package/lib/Btc.d.ts +37 -0
- package/lib/Btc.d.ts.map +1 -1
- package/lib/Btc.js +30 -2
- package/lib/Btc.js.map +1 -1
- package/lib/BtcNew.d.ts +84 -0
- package/lib/BtcNew.d.ts.map +1 -1
- package/lib/BtcNew.js +326 -9
- package/lib/BtcNew.js.map +1 -1
- package/lib/createTransaction.d.ts.map +1 -1
- package/lib/createTransaction.js +3 -2
- package/lib/createTransaction.js.map +1 -1
- package/lib/getTrustedInputBIP143.d.ts +1 -2
- package/lib/getTrustedInputBIP143.d.ts.map +1 -1
- package/lib/getTrustedInputBIP143.js +1 -1
- package/lib/getTrustedInputBIP143.js.map +1 -1
- package/lib/newops/accounttype.d.ts +3 -3
- package/lib/newops/accounttype.d.ts.map +1 -1
- package/lib/newops/accounttype.js +15 -14
- package/lib/newops/accounttype.js.map +1 -1
- package/lib/newops/appClient.d.ts +1 -1
- package/lib/newops/appClient.d.ts.map +1 -1
- package/lib/newops/clientCommands.js +2 -2
- package/lib/newops/clientCommands.js.map +1 -1
- package/lib/newops/merkelizedPsbt.d.ts +1 -1
- package/lib/newops/merkelizedPsbt.d.ts.map +1 -1
- package/lib/newops/merkelizedPsbt.js +1 -1
- package/lib/newops/merkelizedPsbt.js.map +1 -1
- package/lib/newops/policy.js +2 -2
- package/lib/newops/policy.js.map +1 -1
- package/lib/newops/psbtExtractor.d.ts +1 -1
- package/lib/newops/psbtExtractor.d.ts.map +1 -1
- package/lib/newops/psbtExtractor.js +3 -3
- package/lib/newops/psbtExtractor.js.map +1 -1
- package/lib/newops/psbtFinalizer.d.ts +1 -1
- package/lib/newops/psbtFinalizer.d.ts.map +1 -1
- package/lib/newops/psbtFinalizer.js +5 -6
- package/lib/newops/psbtFinalizer.js.map +1 -1
- package/lib/signP2SHTransaction.d.ts.map +1 -1
- package/lib/signP2SHTransaction.js +3 -2
- package/lib/signP2SHTransaction.js.map +1 -1
- package/lib-es/Btc.d.ts +37 -0
- package/lib-es/Btc.d.ts.map +1 -1
- package/lib-es/Btc.js +30 -2
- package/lib-es/Btc.js.map +1 -1
- package/lib-es/BtcNew.d.ts +84 -0
- package/lib-es/BtcNew.d.ts.map +1 -1
- package/lib-es/BtcNew.js +325 -8
- package/lib-es/BtcNew.js.map +1 -1
- package/lib-es/createTransaction.d.ts.map +1 -1
- package/lib-es/createTransaction.js +3 -2
- package/lib-es/createTransaction.js.map +1 -1
- package/lib-es/getTrustedInputBIP143.d.ts +1 -2
- package/lib-es/getTrustedInputBIP143.d.ts.map +1 -1
- package/lib-es/getTrustedInputBIP143.js +1 -1
- package/lib-es/getTrustedInputBIP143.js.map +1 -1
- package/lib-es/newops/accounttype.d.ts +3 -3
- package/lib-es/newops/accounttype.d.ts.map +1 -1
- package/lib-es/newops/accounttype.js +11 -10
- package/lib-es/newops/accounttype.js.map +1 -1
- package/lib-es/newops/appClient.d.ts +1 -1
- package/lib-es/newops/appClient.d.ts.map +1 -1
- package/lib-es/newops/clientCommands.js +1 -1
- package/lib-es/newops/clientCommands.js.map +1 -1
- package/lib-es/newops/merkelizedPsbt.d.ts +1 -1
- package/lib-es/newops/merkelizedPsbt.d.ts.map +1 -1
- package/lib-es/newops/merkelizedPsbt.js +1 -1
- package/lib-es/newops/merkelizedPsbt.js.map +1 -1
- package/lib-es/newops/policy.js +1 -1
- package/lib-es/newops/policy.js.map +1 -1
- package/lib-es/newops/psbtExtractor.d.ts +1 -1
- package/lib-es/newops/psbtExtractor.d.ts.map +1 -1
- package/lib-es/newops/psbtExtractor.js +1 -1
- package/lib-es/newops/psbtExtractor.js.map +1 -1
- package/lib-es/newops/psbtFinalizer.d.ts +1 -1
- package/lib-es/newops/psbtFinalizer.d.ts.map +1 -1
- package/lib-es/newops/psbtFinalizer.js +1 -2
- package/lib-es/newops/psbtFinalizer.js.map +1 -1
- package/lib-es/signP2SHTransaction.d.ts.map +1 -1
- package/lib-es/signP2SHTransaction.js +3 -2
- package/lib-es/signP2SHTransaction.js.map +1 -1
- package/package.json +6 -6
- package/src/Btc.ts +41 -2
- package/src/BtcNew.ts +483 -9
- package/src/createTransaction.ts +4 -3
- package/src/getTrustedInputBIP143.ts +0 -2
- package/src/newops/accounttype.ts +11 -12
- package/src/newops/appClient.ts +1 -1
- package/src/newops/clientCommands.ts +1 -1
- package/src/newops/merkelizedPsbt.ts +1 -1
- package/src/newops/policy.ts +1 -1
- package/src/newops/psbtExtractor.ts +1 -2
- package/src/newops/psbtFinalizer.ts +1 -2
- package/src/signP2SHTransaction.ts +3 -2
- package/tests/Btc.test.ts +848 -20
- package/tests/newops/BtcNew.signMessage.test.ts +35 -0
- package/tests/newops/BtcNew.signPsbtBuffer.test.ts +391 -0
- package/tests/newops/BtcNew.test.ts +13 -1
- package/tests/newops/integrationtools.ts +1 -1
- package/lib/buffertools.d.ts +0 -31
- package/lib/buffertools.d.ts.map +0 -1
- package/lib/buffertools.js +0 -129
- package/lib/buffertools.js.map +0 -1
- package/lib/newops/psbtv2.d.ts +0 -150
- package/lib/newops/psbtv2.d.ts.map +0 -1
- package/lib/newops/psbtv2.js +0 -469
- package/lib/newops/psbtv2.js.map +0 -1
- package/lib-es/buffertools.d.ts +0 -31
- package/lib-es/buffertools.d.ts.map +0 -1
- package/lib-es/buffertools.js +0 -119
- package/lib-es/buffertools.js.map +0 -1
- package/lib-es/newops/psbtv2.d.ts +0 -150
- package/lib-es/newops/psbtv2.d.ts.map +0 -1
- package/lib-es/newops/psbtv2.js +0 -464
- package/lib-es/newops/psbtv2.js.map +0 -1
- package/src/buffertools.ts +0 -137
- package/src/newops/psbtv2.ts +0 -525
- package/tests/buffertools.test.ts +0 -25
- package/tests/newops/psbtv2.test.ts +0 -15
package/src/BtcNew.ts
CHANGED
|
@@ -1,11 +1,5 @@
|
|
|
1
1
|
import { crypto } from "bitcoinjs-lib";
|
|
2
2
|
import { secp256k1 } from "@noble/curves/secp256k1";
|
|
3
|
-
|
|
4
|
-
// Replacement for pointCompress from tiny-secp256k1
|
|
5
|
-
function pointCompress(point: Uint8Array, compressed = true): Uint8Array {
|
|
6
|
-
const p = secp256k1.ProjectivePoint.fromHex(point);
|
|
7
|
-
return p.toRawBytes(compressed);
|
|
8
|
-
}
|
|
9
3
|
import {
|
|
10
4
|
getXpubComponents,
|
|
11
5
|
hardenedPathOf,
|
|
@@ -13,7 +7,7 @@ import {
|
|
|
13
7
|
pathStringToArray,
|
|
14
8
|
pubkeyFromXpub,
|
|
15
9
|
} from "./bip32";
|
|
16
|
-
import { BufferReader } from "
|
|
10
|
+
import { BufferReader, psbtIn, PsbtV2 } from "@ledgerhq/psbtv2";
|
|
17
11
|
import type { CreateTransactionArg } from "./createTransaction";
|
|
18
12
|
import type { AddressFormat } from "./getWalletPublicKey";
|
|
19
13
|
import {
|
|
@@ -28,10 +22,15 @@ import { AppClient as Client } from "./newops/appClient";
|
|
|
28
22
|
import { createKey, DefaultDescriptorTemplate, WalletPolicy } from "./newops/policy";
|
|
29
23
|
import { extract } from "./newops/psbtExtractor";
|
|
30
24
|
import { finalize } from "./newops/psbtFinalizer";
|
|
31
|
-
import { psbtIn, PsbtV2 } from "./newops/psbtv2";
|
|
32
25
|
import { serializeTransaction } from "./serializeTransaction";
|
|
33
26
|
import type { Transaction } from "./types";
|
|
34
27
|
|
|
28
|
+
// Replacement for pointCompress from tiny-secp256k1
|
|
29
|
+
function pointCompress(point: Uint8Array, compressed = true): Uint8Array {
|
|
30
|
+
const p = secp256k1.ProjectivePoint.fromHex(point);
|
|
31
|
+
return p.toRawBytes(compressed);
|
|
32
|
+
}
|
|
33
|
+
|
|
35
34
|
/**
|
|
36
35
|
* @class BtcNew
|
|
37
36
|
* @description This class implements the same interface as BtcOld (formerly
|
|
@@ -282,7 +281,7 @@ export default class BtcNew {
|
|
|
282
281
|
const progressCallback = () => {
|
|
283
282
|
if (!firstSigned) {
|
|
284
283
|
firstSigned = true;
|
|
285
|
-
arg.onDeviceSignatureGranted
|
|
284
|
+
if (arg.onDeviceSignatureGranted) arg.onDeviceSignatureGranted();
|
|
286
285
|
}
|
|
287
286
|
progress();
|
|
288
287
|
};
|
|
@@ -293,6 +292,481 @@ export default class BtcNew {
|
|
|
293
292
|
return serializedTx.toString("hex");
|
|
294
293
|
}
|
|
295
294
|
|
|
295
|
+
/**
|
|
296
|
+
* Signs a PSBT buffer using the Bitcoin app (new protocol).
|
|
297
|
+
*
|
|
298
|
+
* - If the PSBT is v2, it is deserialized directly.
|
|
299
|
+
* - If the PSBT is v0, it is converted to v2 internally.
|
|
300
|
+
* - The account type (legacy, wrapped segwit, native segwit, taproot) is
|
|
301
|
+
* inferred from PSBT data when possible, or from the provided options.
|
|
302
|
+
*
|
|
303
|
+
* Note: All internal inputs (inputs that can be signed by the device) must
|
|
304
|
+
* belong to the same account and use the same account type. Mixed input types
|
|
305
|
+
* or inputs from different accounts are not supported and will throw an error.
|
|
306
|
+
*
|
|
307
|
+
* @param psbtBuffer - Raw PSBT buffer (v0 or v2) to be signed.
|
|
308
|
+
* @param options - Optional signing configuration.
|
|
309
|
+
* @param options.finalizePsbt - Whether to finalize the PSBT after signing
|
|
310
|
+
* (default: true). If true, the returned `tx` is a fully signed
|
|
311
|
+
* transaction ready for broadcast.
|
|
312
|
+
* @param options.accountPath - BIP32 account path (for example,
|
|
313
|
+
* "m/84'/0'/0'") used when BIP32 derivation information is missing from
|
|
314
|
+
* the PSBT. Required if the PSBT does not contain BIP32 derivation data.
|
|
315
|
+
* @param options.addressFormat - Explicit address format to use when the
|
|
316
|
+
* account type cannot be inferred from the PSBT ("legacy", "p2sh",
|
|
317
|
+
* "bech32", or "bech32m").
|
|
318
|
+
* @param options.onDeviceSignatureRequested - Callback when signature is about to be requested from device.
|
|
319
|
+
* @param options.onDeviceSignatureGranted - Callback when the first signature is granted by device.
|
|
320
|
+
* @param options.onDeviceStreaming - Callback to track signing progress with index and total.
|
|
321
|
+
*
|
|
322
|
+
* @returns An object containing:
|
|
323
|
+
* - `psbt`: a non-finalized PSBT buffer including signatures.
|
|
324
|
+
* - `tx`: the fully signed transaction hex string (if `finalizePsbt` is
|
|
325
|
+
* true), or the hex of the transaction that would be extracted after
|
|
326
|
+
* finalization.
|
|
327
|
+
*/
|
|
328
|
+
async signPsbtBuffer(
|
|
329
|
+
psbtBuffer: Buffer,
|
|
330
|
+
options?: {
|
|
331
|
+
finalizePsbt?: boolean;
|
|
332
|
+
accountPath?: string;
|
|
333
|
+
addressFormat?: AddressFormat;
|
|
334
|
+
onDeviceSignatureRequested?: () => void;
|
|
335
|
+
onDeviceSignatureGranted?: () => void;
|
|
336
|
+
onDeviceStreaming?: (arg: { progress: number; total: number; index: number }) => void;
|
|
337
|
+
},
|
|
338
|
+
) {
|
|
339
|
+
const psbt = this.deserializePsbt(psbtBuffer);
|
|
340
|
+
const inputCount = psbt.getGlobalInputCount();
|
|
341
|
+
|
|
342
|
+
if (inputCount === 0) {
|
|
343
|
+
throw new Error("No inputs in PSBT");
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const masterFp = await this.client.getMasterFingerprint();
|
|
347
|
+
const { accountPath, detectedScriptType, internalInputIndices } = this.analyzeAllInputs(
|
|
348
|
+
psbt,
|
|
349
|
+
inputCount,
|
|
350
|
+
masterFp,
|
|
351
|
+
options?.accountPath,
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
const accountXpub = await this.client.getExtendedPubkey(false, accountPath);
|
|
355
|
+
const referenceInputIndex = internalInputIndices.length > 0 ? internalInputIndices[0] : 0;
|
|
356
|
+
|
|
357
|
+
const accountType = this.determineAccountType(
|
|
358
|
+
psbt,
|
|
359
|
+
referenceInputIndex,
|
|
360
|
+
masterFp,
|
|
361
|
+
detectedScriptType,
|
|
362
|
+
accountPath,
|
|
363
|
+
options?.addressFormat,
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
const walletPolicy = this.createWalletPolicy(masterFp, accountPath, accountXpub, accountType);
|
|
367
|
+
const progressCallback = this.createProgressCallback(inputCount, options);
|
|
368
|
+
|
|
369
|
+
await this.signPsbt(psbt, walletPolicy, progressCallback);
|
|
370
|
+
|
|
371
|
+
return this.finalizePsbtAndExtract(psbt, options?.finalizePsbt);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
private deserializePsbt(psbtBuffer: Buffer): PsbtV2 {
|
|
375
|
+
const psbtVersion = PsbtV2.getPsbtVersionNumber(psbtBuffer);
|
|
376
|
+
const psbt = psbtVersion === 2 ? new PsbtV2() : PsbtV2.fromV0(psbtBuffer, true);
|
|
377
|
+
|
|
378
|
+
if (psbtVersion === 2) {
|
|
379
|
+
psbt.deserialize(psbtBuffer);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return psbt;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
private analyzeAllInputs(
|
|
386
|
+
psbt: PsbtV2,
|
|
387
|
+
inputCount: number,
|
|
388
|
+
masterFp: Buffer,
|
|
389
|
+
accountPathOption?: string,
|
|
390
|
+
): {
|
|
391
|
+
accountPath: number[];
|
|
392
|
+
detectedScriptType: string | undefined;
|
|
393
|
+
internalInputIndices: number[];
|
|
394
|
+
} {
|
|
395
|
+
const internalInputIndices: number[] = [];
|
|
396
|
+
let accountPath: number[] = [];
|
|
397
|
+
let detectedScriptType: string | undefined;
|
|
398
|
+
|
|
399
|
+
for (let i = 0; i < inputCount; i++) {
|
|
400
|
+
const inputInfo = this.analyzeInput(psbt, i, masterFp);
|
|
401
|
+
|
|
402
|
+
if (!inputInfo.isInternal) {
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
internalInputIndices.push(i);
|
|
407
|
+
this.validateAccountPathConsistency(accountPath, inputInfo.accountPath, i);
|
|
408
|
+
|
|
409
|
+
if (accountPath.length === 0) {
|
|
410
|
+
accountPath = inputInfo.accountPath;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
this.validateScriptTypeConsistency(detectedScriptType, inputInfo.scriptType, i);
|
|
414
|
+
|
|
415
|
+
if (!detectedScriptType) {
|
|
416
|
+
detectedScriptType = inputInfo.scriptType;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (internalInputIndices.length === 0) {
|
|
421
|
+
accountPath = this.resolveAccountPathFromOptions(accountPathOption);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return { accountPath, detectedScriptType, internalInputIndices };
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
private validateAccountPathConsistency(
|
|
428
|
+
accountPath: number[],
|
|
429
|
+
newAccountPath: number[],
|
|
430
|
+
inputIndex: number,
|
|
431
|
+
): void {
|
|
432
|
+
if (accountPath.length > 0 && !this.arePathsEqual(accountPath, newAccountPath)) {
|
|
433
|
+
throw new Error(
|
|
434
|
+
`Mixed accounts detected in PSBT. Input ${inputIndex} uses account path ` +
|
|
435
|
+
`${pathArrayToString(newAccountPath)} but expected ` +
|
|
436
|
+
`${pathArrayToString(accountPath)}. All internal inputs must belong to the same account.`,
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
private validateScriptTypeConsistency(
|
|
442
|
+
detectedScriptType: string | undefined,
|
|
443
|
+
newScriptType: string | undefined,
|
|
444
|
+
inputIndex: number,
|
|
445
|
+
): void {
|
|
446
|
+
if (detectedScriptType && newScriptType && detectedScriptType !== newScriptType) {
|
|
447
|
+
throw new Error(
|
|
448
|
+
`Mixed input types detected in PSBT. Input ${inputIndex} uses ${newScriptType} ` +
|
|
449
|
+
`but expected ${detectedScriptType}. All internal inputs must use the same script type.`,
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
private resolveAccountPathFromOptions(accountPathOption?: string): number[] {
|
|
455
|
+
if (!accountPathOption) {
|
|
456
|
+
throw new Error(
|
|
457
|
+
"No internal inputs found in PSBT (no BIP32 derivation matching device fingerprint) " +
|
|
458
|
+
"and no account path provided in options. Please provide accountPath in options " +
|
|
459
|
+
"(e.g., \"m/84'/0'/0'\" for native segwit)",
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
return pathStringToArray(accountPathOption);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
private createWalletPolicy(
|
|
466
|
+
masterFp: Buffer,
|
|
467
|
+
accountPath: number[],
|
|
468
|
+
accountXpub: string,
|
|
469
|
+
accountType: AccountType,
|
|
470
|
+
): WalletPolicy {
|
|
471
|
+
const key = createKey(masterFp, accountPath, accountXpub);
|
|
472
|
+
return new WalletPolicy(accountType.getDescriptorTemplate(), key);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
private createProgressCallback(
|
|
476
|
+
inputCount: number,
|
|
477
|
+
options?: {
|
|
478
|
+
onDeviceSignatureRequested?: () => void;
|
|
479
|
+
onDeviceSignatureGranted?: () => void;
|
|
480
|
+
onDeviceStreaming?: (arg: { progress: number; total: number; index: number }) => void;
|
|
481
|
+
},
|
|
482
|
+
): () => void {
|
|
483
|
+
let notifyCount = 0;
|
|
484
|
+
let firstSigned = false;
|
|
485
|
+
|
|
486
|
+
const progress = () => {
|
|
487
|
+
if (!options?.onDeviceStreaming) return;
|
|
488
|
+
options.onDeviceStreaming({
|
|
489
|
+
total: 2 * inputCount,
|
|
490
|
+
index: notifyCount,
|
|
491
|
+
progress: ++notifyCount / (2 * inputCount),
|
|
492
|
+
});
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
if (options?.onDeviceSignatureRequested) options.onDeviceSignatureRequested();
|
|
496
|
+
|
|
497
|
+
return () => {
|
|
498
|
+
if (!firstSigned) {
|
|
499
|
+
firstSigned = true;
|
|
500
|
+
if (options?.onDeviceSignatureGranted) options.onDeviceSignatureGranted();
|
|
501
|
+
}
|
|
502
|
+
progress();
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
private finalizePsbtAndExtract(
|
|
507
|
+
psbt: PsbtV2,
|
|
508
|
+
shouldFinalize?: boolean,
|
|
509
|
+
): { psbt: Buffer; tx: string } {
|
|
510
|
+
if (shouldFinalize ?? true) {
|
|
511
|
+
finalize(psbt);
|
|
512
|
+
}
|
|
513
|
+
const serializedTx = extract(psbt);
|
|
514
|
+
|
|
515
|
+
return {
|
|
516
|
+
psbt: psbt.serialize(),
|
|
517
|
+
tx: serializedTx.toString("hex"),
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Analyzes a single input to determine if it's internal (can be signed by the device)
|
|
523
|
+
* and extracts its account path and script type.
|
|
524
|
+
*/
|
|
525
|
+
private analyzeInput(
|
|
526
|
+
psbt: PsbtV2,
|
|
527
|
+
inputIndex: number,
|
|
528
|
+
masterFp: Buffer,
|
|
529
|
+
): {
|
|
530
|
+
isInternal: boolean;
|
|
531
|
+
accountPath: number[];
|
|
532
|
+
scriptType: string | undefined;
|
|
533
|
+
} {
|
|
534
|
+
const derivationResult = this.checkBip32Derivation(psbt, inputIndex, masterFp);
|
|
535
|
+
const scriptType = this.determineInputScriptType(psbt, inputIndex);
|
|
536
|
+
|
|
537
|
+
return {
|
|
538
|
+
isInternal: derivationResult.isInternal,
|
|
539
|
+
accountPath: derivationResult.accountPath,
|
|
540
|
+
scriptType,
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
private checkBip32Derivation(
|
|
545
|
+
psbt: PsbtV2,
|
|
546
|
+
inputIndex: number,
|
|
547
|
+
masterFp: Buffer,
|
|
548
|
+
): { isInternal: boolean; accountPath: number[] } {
|
|
549
|
+
// Check standard BIP32 derivation
|
|
550
|
+
const standardResult = this.checkStandardBip32Derivation(psbt, inputIndex, masterFp);
|
|
551
|
+
if (standardResult.isInternal) {
|
|
552
|
+
return standardResult;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Check TAP_BIP32_DERIVATION for taproot inputs
|
|
556
|
+
return this.checkTaprootBip32Derivation(psbt, inputIndex, masterFp);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
private checkStandardBip32Derivation(
|
|
560
|
+
psbt: PsbtV2,
|
|
561
|
+
inputIndex: number,
|
|
562
|
+
masterFp: Buffer,
|
|
563
|
+
): { isInternal: boolean; accountPath: number[] } {
|
|
564
|
+
const keyDatas = psbt.getInputKeyDatas(inputIndex, psbtIn.BIP32_DERIVATION);
|
|
565
|
+
|
|
566
|
+
for (const pubkey of keyDatas) {
|
|
567
|
+
const derivationInfo = psbt.getInputBip32Derivation(inputIndex, pubkey);
|
|
568
|
+
if (derivationInfo?.masterFingerprint.equals(masterFp)) {
|
|
569
|
+
return this.extractAccountPath(derivationInfo.path);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
return { isInternal: false, accountPath: [] };
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
private checkTaprootBip32Derivation(
|
|
577
|
+
psbt: PsbtV2,
|
|
578
|
+
inputIndex: number,
|
|
579
|
+
masterFp: Buffer,
|
|
580
|
+
): { isInternal: boolean; accountPath: number[] } {
|
|
581
|
+
const tapKeyDatas = psbt.getInputKeyDatas(inputIndex, psbtIn.TAP_BIP32_DERIVATION);
|
|
582
|
+
|
|
583
|
+
for (const pubkey of tapKeyDatas) {
|
|
584
|
+
const derivationInfo = psbt.getInputTapBip32Derivation(inputIndex, pubkey);
|
|
585
|
+
if (derivationInfo?.masterFingerprint.equals(masterFp)) {
|
|
586
|
+
return this.extractAccountPath(derivationInfo.path);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
return { isInternal: false, accountPath: [] };
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
private extractAccountPath(fullPath: number[]): { isInternal: true; accountPath: number[] } {
|
|
594
|
+
// Extract account path (full path minus last 2 elements for change/index)
|
|
595
|
+
const accountPath = fullPath.length >= 2 ? fullPath.slice(0, -2) : [];
|
|
596
|
+
return { isInternal: true, accountPath };
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
private determineInputScriptType(psbt: PsbtV2, inputIndex: number): string | undefined {
|
|
600
|
+
const witnessUtxo = psbt.getInputWitnessUtxo(inputIndex);
|
|
601
|
+
if (witnessUtxo) {
|
|
602
|
+
return this.detectScriptType(witnessUtxo.scriptPubKey);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
const redeemScript = psbt.getInputRedeemScript(inputIndex);
|
|
606
|
+
if (redeemScript) {
|
|
607
|
+
return "p2sh-p2wpkh";
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
return undefined;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* Detects the script type from a scriptPubKey.
|
|
615
|
+
*/
|
|
616
|
+
private detectScriptType(scriptPubKey: Buffer): string | undefined {
|
|
617
|
+
if (scriptPubKey.length === 22 && scriptPubKey[0] === 0x00 && scriptPubKey[1] === 0x14) {
|
|
618
|
+
return "p2wpkh";
|
|
619
|
+
}
|
|
620
|
+
if (scriptPubKey.length === 34 && scriptPubKey[0] === 0x51 && scriptPubKey[1] === 0x20) {
|
|
621
|
+
return "p2tr";
|
|
622
|
+
}
|
|
623
|
+
if (scriptPubKey.length === 23 && scriptPubKey[0] === 0xa9 && scriptPubKey[22] === 0x87) {
|
|
624
|
+
return "p2sh";
|
|
625
|
+
}
|
|
626
|
+
if (scriptPubKey.length === 25 && scriptPubKey[0] === 0x76 && scriptPubKey[1] === 0xa9) {
|
|
627
|
+
return "p2pkh";
|
|
628
|
+
}
|
|
629
|
+
return undefined;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* Compares two derivation paths for equality.
|
|
634
|
+
*/
|
|
635
|
+
private arePathsEqual(path1: number[], path2: number[]): boolean {
|
|
636
|
+
if (path1.length !== path2.length) return false;
|
|
637
|
+
return path1.every((elem, idx) => elem === path2[idx]);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* Determines the account type based on detected script type, account path, or options.
|
|
642
|
+
*/
|
|
643
|
+
private determineAccountType(
|
|
644
|
+
psbt: PsbtV2,
|
|
645
|
+
inputIndex: number,
|
|
646
|
+
masterFp: Buffer,
|
|
647
|
+
detectedScriptType: string | undefined,
|
|
648
|
+
accountPath: number[],
|
|
649
|
+
addressFormat?: AddressFormat,
|
|
650
|
+
): AccountType {
|
|
651
|
+
// Use detected script type if available
|
|
652
|
+
if (detectedScriptType) {
|
|
653
|
+
return this.createAccountTypeFromScriptType(detectedScriptType, psbt, masterFp);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// Fall back to witness UTXO analysis
|
|
657
|
+
const witnessUtxoType = this.determineAccountTypeFromWitnessUtxo(psbt, inputIndex, masterFp);
|
|
658
|
+
if (witnessUtxoType) {
|
|
659
|
+
return witnessUtxoType;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// Try redeem script
|
|
663
|
+
if (psbt.getInputRedeemScript(inputIndex)) {
|
|
664
|
+
return new p2wpkhWrapped(psbt, masterFp);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// Use address format option
|
|
668
|
+
if (addressFormat) {
|
|
669
|
+
return this.createAccountTypeFromAddressFormat(addressFormat, psbt, masterFp);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// Infer from account path purpose
|
|
673
|
+
const purposeBasedType = this.determineAccountTypeFromPurpose(accountPath, psbt, masterFp);
|
|
674
|
+
if (purposeBasedType) {
|
|
675
|
+
return purposeBasedType;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// Default to native segwit
|
|
679
|
+
return new p2wpkh(psbt, masterFp);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
private createAccountTypeFromScriptType(
|
|
683
|
+
scriptType: string,
|
|
684
|
+
psbt: PsbtV2,
|
|
685
|
+
masterFp: Buffer,
|
|
686
|
+
): AccountType {
|
|
687
|
+
switch (scriptType) {
|
|
688
|
+
case "p2wpkh":
|
|
689
|
+
return new p2wpkh(psbt, masterFp);
|
|
690
|
+
case "p2tr":
|
|
691
|
+
return new p2tr(psbt, masterFp);
|
|
692
|
+
case "p2sh":
|
|
693
|
+
case "p2sh-p2wpkh":
|
|
694
|
+
return new p2wpkhWrapped(psbt, masterFp);
|
|
695
|
+
case "p2pkh":
|
|
696
|
+
return new p2pkh(psbt, masterFp);
|
|
697
|
+
default:
|
|
698
|
+
return new p2wpkh(psbt, masterFp);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
private determineAccountTypeFromWitnessUtxo(
|
|
703
|
+
psbt: PsbtV2,
|
|
704
|
+
inputIndex: number,
|
|
705
|
+
masterFp: Buffer,
|
|
706
|
+
): AccountType | null {
|
|
707
|
+
const witnessUtxo = psbt.getInputWitnessUtxo(inputIndex);
|
|
708
|
+
if (!witnessUtxo) {
|
|
709
|
+
return null;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
const scriptPubKey = witnessUtxo.scriptPubKey;
|
|
713
|
+
|
|
714
|
+
if (scriptPubKey.length === 22 && scriptPubKey[0] === 0x00 && scriptPubKey[1] === 0x14) {
|
|
715
|
+
return new p2wpkh(psbt, masterFp);
|
|
716
|
+
}
|
|
717
|
+
if (scriptPubKey.length === 34 && scriptPubKey[0] === 0x51 && scriptPubKey[1] === 0x20) {
|
|
718
|
+
return new p2tr(psbt, masterFp);
|
|
719
|
+
}
|
|
720
|
+
if (scriptPubKey.length === 23 && scriptPubKey[0] === 0xa9 && scriptPubKey[22] === 0x87) {
|
|
721
|
+
return new p2wpkhWrapped(psbt, masterFp);
|
|
722
|
+
}
|
|
723
|
+
if (scriptPubKey.length === 25 && scriptPubKey[0] === 0x76 && scriptPubKey[1] === 0xa9) {
|
|
724
|
+
return new p2pkh(psbt, masterFp);
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
throw new Error(`Unsupported script type: ${scriptPubKey.toString("hex")}`);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
private createAccountTypeFromAddressFormat(
|
|
731
|
+
addressFormat: AddressFormat,
|
|
732
|
+
psbt: PsbtV2,
|
|
733
|
+
masterFp: Buffer,
|
|
734
|
+
): AccountType {
|
|
735
|
+
const descrTemplate = descrTemplFrom(addressFormat);
|
|
736
|
+
|
|
737
|
+
if (descrTemplate === "pkh(@0)") return new p2pkh(psbt, masterFp);
|
|
738
|
+
if (descrTemplate === "wpkh(@0)") return new p2wpkh(psbt, masterFp);
|
|
739
|
+
if (descrTemplate === "sh(wpkh(@0))") return new p2wpkhWrapped(psbt, masterFp);
|
|
740
|
+
if (descrTemplate === "tr(@0)") return new p2tr(psbt, masterFp);
|
|
741
|
+
|
|
742
|
+
throw new Error(`Unsupported descriptor template: ${descrTemplate}`);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
private determineAccountTypeFromPurpose(
|
|
746
|
+
accountPath: number[],
|
|
747
|
+
psbt: PsbtV2,
|
|
748
|
+
masterFp: Buffer,
|
|
749
|
+
): AccountType | null {
|
|
750
|
+
if (accountPath.length < 1) {
|
|
751
|
+
return null;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
const purpose = accountPath[0] - 0x80000000;
|
|
755
|
+
|
|
756
|
+
switch (purpose) {
|
|
757
|
+
case 44:
|
|
758
|
+
return new p2pkh(psbt, masterFp);
|
|
759
|
+
case 49:
|
|
760
|
+
return new p2wpkhWrapped(psbt, masterFp);
|
|
761
|
+
case 84:
|
|
762
|
+
return new p2wpkh(psbt, masterFp);
|
|
763
|
+
case 86:
|
|
764
|
+
return new p2tr(psbt, masterFp);
|
|
765
|
+
default:
|
|
766
|
+
return null;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
296
770
|
/**
|
|
297
771
|
* Signs an arbitrary hex-formatted message with the private key at
|
|
298
772
|
* the provided derivation path according to the Bitcoin Signature format
|
package/src/createTransaction.ts
CHANGED
|
@@ -202,8 +202,6 @@ export async function createTransaction(
|
|
|
202
202
|
version: defaultVersion,
|
|
203
203
|
timestamp: Buffer.alloc(0),
|
|
204
204
|
};
|
|
205
|
-
const getTrustedInputCall =
|
|
206
|
-
useBip143 && !useTrustedInputForSegwit ? getTrustedInputBIP143 : getTrustedInput;
|
|
207
205
|
const outputScript = Buffer.from(outputScriptHex, "hex");
|
|
208
206
|
notify(0, 0);
|
|
209
207
|
// first pass on inputs to get trusted inputs
|
|
@@ -212,7 +210,10 @@ export async function createTransaction(
|
|
|
212
210
|
if (isZcash) {
|
|
213
211
|
input[0].consensusBranchId = getZcashBranchId(input[4]);
|
|
214
212
|
}
|
|
215
|
-
const trustedInput =
|
|
213
|
+
const trustedInput =
|
|
214
|
+
useBip143 && !useTrustedInputForSegwit
|
|
215
|
+
? getTrustedInputBIP143(input[1], input[0], additionals)
|
|
216
|
+
: await getTrustedInput(transport, input[1], input[0], additionals);
|
|
216
217
|
log("hw", "got trustedInput=" + trustedInput);
|
|
217
218
|
const sequence = Buffer.alloc(4);
|
|
218
219
|
sequence.writeUInt32LE(
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
import Transport from "@ledgerhq/hw-transport";
|
|
2
1
|
import { sha256 } from "@noble/hashes/sha256";
|
|
3
2
|
import type { Transaction } from "./types";
|
|
4
3
|
import { serializeTransaction } from "./serializeTransaction";
|
|
5
4
|
export function getTrustedInputBIP143(
|
|
6
|
-
transport: Transport,
|
|
7
5
|
indexLookup: number,
|
|
8
6
|
transaction: Transaction,
|
|
9
7
|
additionals: Array<string> = [],
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { crypto } from "bitcoinjs-lib";
|
|
2
2
|
import { secp256k1 } from "@noble/curves/secp256k1";
|
|
3
|
+
import { BufferWriter, PsbtV2 } from "@ledgerhq/psbtv2";
|
|
4
|
+
import { HASH_SIZE, OP_CHECKSIG, OP_DUP, OP_EQUAL, OP_EQUALVERIFY, OP_HASH160 } from "../constants";
|
|
5
|
+
import { hashPublicKey } from "../hashPublicKey";
|
|
6
|
+
import { DefaultDescriptorTemplate } from "./policy";
|
|
3
7
|
|
|
4
8
|
// Helper function to convert bytes to bigint for scalar operations
|
|
5
9
|
function bytesToBigInt(bytes: Uint8Array): bigint {
|
|
@@ -20,11 +24,6 @@ function pointAddScalar(point: Uint8Array, scalar: Uint8Array): Uint8Array | nul
|
|
|
20
24
|
return null;
|
|
21
25
|
}
|
|
22
26
|
}
|
|
23
|
-
import { BufferWriter } from "../buffertools";
|
|
24
|
-
import { HASH_SIZE, OP_CHECKSIG, OP_DUP, OP_EQUAL, OP_EQUALVERIFY, OP_HASH160 } from "../constants";
|
|
25
|
-
import { hashPublicKey } from "../hashPublicKey";
|
|
26
|
-
import { DefaultDescriptorTemplate } from "./policy";
|
|
27
|
-
import { PsbtV2 } from "./psbtv2";
|
|
28
27
|
|
|
29
28
|
export type SpendingCondition = {
|
|
30
29
|
scriptPubKey: Buffer;
|
|
@@ -175,7 +174,7 @@ export class p2pkh extends SingleKeyAccount {
|
|
|
175
174
|
this.psbt.setInputBip32Derivation(i, pubkey, this.masterFp, path);
|
|
176
175
|
}
|
|
177
176
|
|
|
178
|
-
setSingleKeyOutput(i: number,
|
|
177
|
+
setSingleKeyOutput(i: number, _cond: SpendingCondition, pubkey: Buffer, path: number[]) {
|
|
179
178
|
this.psbt.setOutputBip32Derivation(i, pubkey, this.masterFp, path);
|
|
180
179
|
}
|
|
181
180
|
|
|
@@ -186,7 +185,7 @@ export class p2pkh extends SingleKeyAccount {
|
|
|
186
185
|
|
|
187
186
|
export class p2tr extends SingleKeyAccount {
|
|
188
187
|
singleKeyCondition(pubkey: Buffer): SpendingCondition {
|
|
189
|
-
const xonlyPubkey = pubkey.
|
|
188
|
+
const xonlyPubkey = pubkey.subarray(1); // x-only pubkey
|
|
190
189
|
const buf = new BufferWriter();
|
|
191
190
|
const outputKey = this.getTaprootOutputKey(xonlyPubkey);
|
|
192
191
|
buf.writeSlice(Buffer.from([0x51, 32])); // push1, pubkeylen
|
|
@@ -201,13 +200,13 @@ export class p2tr extends SingleKeyAccount {
|
|
|
201
200
|
pubkey: Buffer,
|
|
202
201
|
path: number[],
|
|
203
202
|
) {
|
|
204
|
-
const xonly = pubkey.
|
|
203
|
+
const xonly = pubkey.subarray(1);
|
|
205
204
|
this.psbt.setInputTapBip32Derivation(i, xonly, [], this.masterFp, path);
|
|
206
205
|
this.psbt.setInputWitnessUtxo(i, spentOutput.amount, spentOutput.cond.scriptPubKey);
|
|
207
206
|
}
|
|
208
207
|
|
|
209
|
-
setSingleKeyOutput(i: number,
|
|
210
|
-
const xonly = pubkey.
|
|
208
|
+
setSingleKeyOutput(i: number, _cond: SpendingCondition, pubkey: Buffer, path: number[]) {
|
|
209
|
+
const xonly = pubkey.subarray(1);
|
|
211
210
|
this.psbt.setOutputTapBip32Derivation(i, xonly, [], this.masterFp, path);
|
|
212
211
|
}
|
|
213
212
|
|
|
@@ -251,7 +250,7 @@ export class p2tr extends SingleKeyAccount {
|
|
|
251
250
|
if (!tweakedKey) throw new Error("Point addition failed");
|
|
252
251
|
const outputEcdsaKey = Buffer.from(tweakedKey);
|
|
253
252
|
// Convert to schnorr.
|
|
254
|
-
const outputSchnorrKey = outputEcdsaKey.
|
|
253
|
+
const outputSchnorrKey = outputEcdsaKey.subarray(1);
|
|
255
254
|
// Create address
|
|
256
255
|
return outputSchnorrKey;
|
|
257
256
|
}
|
|
@@ -295,7 +294,7 @@ export class p2wpkhWrapped extends SingleKeyAccount {
|
|
|
295
294
|
}
|
|
296
295
|
|
|
297
296
|
setSingleKeyOutput(i: number, cond: SpendingCondition, pubkey: Buffer, path: number[]) {
|
|
298
|
-
this.psbt.setOutputRedeemScript(i, cond.redeemScript
|
|
297
|
+
if (cond.redeemScript) this.psbt.setOutputRedeemScript(i, cond.redeemScript);
|
|
299
298
|
this.psbt.setOutputBip32Derivation(i, pubkey, this.masterFp, path);
|
|
300
299
|
}
|
|
301
300
|
|
package/src/newops/appClient.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import Transport from "@ledgerhq/hw-transport";
|
|
2
2
|
import { pathElementsToBuffer } from "../bip32";
|
|
3
|
-
import { PsbtV2 } from "
|
|
3
|
+
import { PsbtV2 } from "@ledgerhq/psbtv2";
|
|
4
4
|
import { MerkelizedPsbt } from "./merkelizedPsbt";
|
|
5
5
|
import { ClientCommandInterpreter } from "./clientCommands";
|
|
6
6
|
import { WalletPolicy } from "./policy";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { crypto } from "bitcoinjs-lib";
|
|
2
|
-
import { BufferReader } from "
|
|
2
|
+
import { BufferReader } from "@ledgerhq/psbtv2";
|
|
3
3
|
import { createVarint } from "../varint";
|
|
4
4
|
import { hashLeaf, Merkle } from "./merkle";
|
|
5
5
|
import { MerkleMap } from "./merkleMap";
|
package/src/newops/policy.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { crypto } from "bitcoinjs-lib";
|
|
2
2
|
import { pathArrayToString } from "../bip32";
|
|
3
|
-
import { BufferWriter } from "
|
|
3
|
+
import { BufferWriter } from "@ledgerhq/psbtv2";
|
|
4
4
|
import { hashLeaf, Merkle } from "./merkle";
|
|
5
5
|
|
|
6
6
|
export type DefaultDescriptorTemplate = "pkh(@0)" | "sh(wpkh(@0))" | "wpkh(@0)" | "tr(@0)";
|