@ledgerhq/psbtv2 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +4 -0
- package/LICENSE.txt +21 -0
- package/README.md +91 -0
- package/jest.config.js +26 -0
- package/lib/buffertools.d.ts +29 -0
- package/lib/buffertools.d.ts.map +1 -0
- package/lib/buffertools.js +129 -0
- package/lib/buffertools.js.map +1 -0
- package/lib/index.d.ts +4 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +18 -0
- package/lib/index.js.map +1 -0
- package/lib/psbtParsing.d.ts +15 -0
- package/lib/psbtParsing.d.ts.map +1 -0
- package/lib/psbtParsing.js +52 -0
- package/lib/psbtParsing.js.map +1 -0
- package/lib/psbtv2.d.ts +200 -0
- package/lib/psbtv2.d.ts.map +1 -0
- package/lib/psbtv2.js +647 -0
- package/lib/psbtv2.js.map +1 -0
- package/lib-es/buffertools.d.ts +29 -0
- package/lib-es/buffertools.d.ts.map +1 -0
- package/lib-es/buffertools.js +119 -0
- package/lib-es/buffertools.js.map +1 -0
- package/lib-es/index.d.ts +4 -0
- package/lib-es/index.d.ts.map +1 -0
- package/lib-es/index.js +4 -0
- package/lib-es/index.js.map +1 -0
- package/lib-es/psbtParsing.d.ts +15 -0
- package/lib-es/psbtParsing.d.ts.map +1 -0
- package/lib-es/psbtParsing.js +48 -0
- package/lib-es/psbtParsing.js.map +1 -0
- package/lib-es/psbtv2.d.ts +200 -0
- package/lib-es/psbtv2.d.ts.map +1 -0
- package/lib-es/psbtv2.js +641 -0
- package/lib-es/psbtv2.js.map +1 -0
- package/package.json +78 -0
- package/src/buffertools.test.ts +116 -0
- package/src/buffertools.ts +137 -0
- package/src/fromV0.test.ts +577 -0
- package/src/index.ts +3 -0
- package/src/psbtParsing.test.ts +86 -0
- package/src/psbtParsing.ts +51 -0
- package/src/psbtv2.test.ts +441 -0
- package/src/psbtv2.ts +740 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalize possible PSBT inputs: base64 (with whitespace/URL-safe), or raw hex.
|
|
3
|
+
*
|
|
4
|
+
* This is a low-level helper; callers that need strict validation and
|
|
5
|
+
* user-facing error messages should use `parsePsbt` instead.
|
|
6
|
+
*/
|
|
7
|
+
export function normalizeToBuffer(psbtMaybe: string): Buffer | null {
|
|
8
|
+
if (!psbtMaybe) return null;
|
|
9
|
+
const s = psbtMaybe.trim();
|
|
10
|
+
|
|
11
|
+
// If hex (even length, only [0-9a-fA-F]) treat as hex
|
|
12
|
+
if (/^[0-9a-fA-F]+$/.test(s) && s.length % 2 === 0) {
|
|
13
|
+
try {
|
|
14
|
+
return Buffer.from(s, "hex");
|
|
15
|
+
} catch {
|
|
16
|
+
/* ignore and fall through to base64 */
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Treat as base64: strip whitespace and convert URL-safe to standard
|
|
21
|
+
const b64 = s.replaceAll(/\s+/g, "").replaceAll("-", "+").replaceAll("_", "/");
|
|
22
|
+
// pad base64
|
|
23
|
+
const pad = b64.length % 4;
|
|
24
|
+
const padded = pad ? b64 + "=".repeat(4 - pad) : b64;
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
return Buffer.from(padded, "base64");
|
|
28
|
+
} catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Parse a PSBT string into a Buffer, throwing on clearly invalid inputs.
|
|
35
|
+
*
|
|
36
|
+
* This wraps `normalizeToBuffer` but preserves existing error messages used in
|
|
37
|
+
* ledger-live when encountering invalid PSBT payloads.
|
|
38
|
+
*/
|
|
39
|
+
export function parsePsbt(psbt: string): Buffer {
|
|
40
|
+
const buf = normalizeToBuffer(psbt);
|
|
41
|
+
|
|
42
|
+
if (!buf) {
|
|
43
|
+
throw new Error("Invalid PSBT: not valid base64");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!buf.length) {
|
|
47
|
+
throw new Error("Invalid PSBT: empty buffer");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return buf;
|
|
51
|
+
}
|
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
import { NoSuchEntry, PsbtV2, psbtGlobal, psbtIn, parseBip32Path } from "./psbtv2";
|
|
2
|
+
import { Psbt, networks } from "bitcoinjs-lib";
|
|
3
|
+
|
|
4
|
+
describe("PsbtV2", () => {
|
|
5
|
+
it("deserializes a psbt and reserializes it unchanged", async () => {
|
|
6
|
+
const psbtBuf = Buffer.from(
|
|
7
|
+
"cHNidP8BAAoBAAAAAAAAAAAAAQIEAgAAAAEDBAAAAAABBAECAQUBAgH7BAIAAAAAAQBxAgAAAAGTarLgEHL3k8/kyXdU3hth/gPn22U2yLLyHdC1dCxIRQEAAAAA/v///wLe4ccAAAAAABYAFOt418QL8QY7Dj/OKcNWW2ichVmrECcAAAAAAAAWABQjGNZvhP71xIdfkzsDjcY4MfjaE/mXHgABAR8QJwAAAAAAABYAFCMY1m+E/vXEh1+TOwONxjgx+NoTIgYDRV7nztyXsLpDW4AGb8ksljo0xgAxeYHRNTMMTuQ6x6MY9azC/VQAAIABAACAAAAAgAAAAAABAAAAAQ4gniz+J/Cth7eKI31ddAXUowZmyjYdWFpGew3+QiYrTbQBDwQBAAAAARAE/f///wESBAAAAAAAAQBxAQAAAAEORx706Sway1HvyGYPjT9pk26pybK/9y/5vIHFHvz0ZAEAAAAAAAAAAAJgrgoAAAAAABYAFDXG4N1tPISxa6iF3Kc6yGPQtZPsrwYyAAAAAAAWABTcKG4M0ua9N86+nsNJ+18IkFZy/AAAAAABAR9grgoAAAAAABYAFDXG4N1tPISxa6iF3Kc6yGPQtZPsIgYCcbW3ea2HCDhYd5e89vDHrsWr52pwnXJPSNLibPh08KAY9azC/VQAAIABAACAAAAAgAEAAAAAAAAAAQ4gr7+uBlkPdB/xr1m2rEYRJjNqTEqC21U99v76tzesM/MBDwQAAAAAARAE/f///wESBAAAAAAAIgICKexHcnEx7SWIogxG7amrt9qm9J/VC6/nC5xappYcTswY9azC/VQAAIABAACAAAAAgAEAAAAKAAAAAQMIqDoGAAAAAAABBBYAFOs4+puBKPgfJule2wxf+uqDaQ/kAAEDCOCTBAAAAAAAAQQiACA/qWbJ3c3C/ZbkpeG8dlufr2zos+tPEQSq1r33cyTlvgA=",
|
|
8
|
+
"base64",
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
const psbt = new PsbtV2();
|
|
12
|
+
psbt.deserialize(psbtBuf);
|
|
13
|
+
|
|
14
|
+
expect(psbt.serialize()).toEqual(psbtBuf);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe("getPsbtVersionNumber", () => {
|
|
18
|
+
it("should return 2 for a PSBTv2 buffer", () => {
|
|
19
|
+
// Create a PSBTv2
|
|
20
|
+
const psbtv2 = new PsbtV2();
|
|
21
|
+
psbtv2.setGlobalTxVersion(2);
|
|
22
|
+
psbtv2.setGlobalInputCount(0);
|
|
23
|
+
psbtv2.setGlobalOutputCount(0);
|
|
24
|
+
psbtv2.setGlobalPsbtVersion(2);
|
|
25
|
+
|
|
26
|
+
const buffer = psbtv2.serialize();
|
|
27
|
+
const version = PsbtV2.getPsbtVersionNumber(buffer);
|
|
28
|
+
|
|
29
|
+
expect(version).toBe(2);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("should return 0 for a PSBTv0 buffer (no version field)", () => {
|
|
33
|
+
// Create a PSBTv0 using bitcoinjs-lib
|
|
34
|
+
const psbtv0 = new Psbt({ network: networks.testnet });
|
|
35
|
+
psbtv0.addInput({
|
|
36
|
+
//@ts-expect-error TransactionInput interface is not declared correctly in bip174 lib
|
|
37
|
+
hash: Buffer.alloc(32, 0),
|
|
38
|
+
index: 0,
|
|
39
|
+
witnessUtxo: {
|
|
40
|
+
script: Buffer.from("0014" + "00".repeat(20), "hex"),
|
|
41
|
+
value: 100000,
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
psbtv0.addOutput({
|
|
45
|
+
script: Buffer.from("0014" + "11".repeat(20), "hex"),
|
|
46
|
+
value: 90000,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const buffer = psbtv0.toBuffer();
|
|
50
|
+
const version = PsbtV2.getPsbtVersionNumber(buffer);
|
|
51
|
+
|
|
52
|
+
expect(version).toBe(0);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("should return 0 for a minimal PSBTv0", () => {
|
|
56
|
+
// Minimal PSBTv0: magic bytes + empty global map (0x00) + empty input/output
|
|
57
|
+
const psbtv0 = new Psbt({ network: networks.testnet });
|
|
58
|
+
psbtv0.addInput({
|
|
59
|
+
//@ts-expect-error TransactionInput interface is not declared correctly in bip174 lib
|
|
60
|
+
hash: Buffer.alloc(32, 1),
|
|
61
|
+
index: 0,
|
|
62
|
+
witnessUtxo: {
|
|
63
|
+
script: Buffer.from("001400000000000000000000000000000000000000000000", "hex"),
|
|
64
|
+
value: 1000,
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
psbtv0.addOutput({
|
|
68
|
+
script: Buffer.from("001400000000000000000000000000000000000000000000", "hex"),
|
|
69
|
+
value: 900,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const buffer = psbtv0.toBuffer();
|
|
73
|
+
const version = PsbtV2.getPsbtVersionNumber(buffer);
|
|
74
|
+
|
|
75
|
+
expect(version).toBe(0);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("should correctly identify version in a converted PSBTv2", () => {
|
|
79
|
+
// Create a PSBTv0 and convert it to PSBTv2
|
|
80
|
+
const psbtv0 = new Psbt({ network: networks.testnet });
|
|
81
|
+
psbtv0.addInput({
|
|
82
|
+
//@ts-expect-error TransactionInput interface is not declared correctly in bip174 lib
|
|
83
|
+
hash: Buffer.alloc(32, 0),
|
|
84
|
+
index: 0,
|
|
85
|
+
witnessUtxo: {
|
|
86
|
+
script: Buffer.from("0014" + "00".repeat(20), "hex"),
|
|
87
|
+
value: 100000,
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
psbtv0.addOutput({
|
|
91
|
+
script: Buffer.from("0014" + "11".repeat(20), "hex"),
|
|
92
|
+
value: 90000,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const psbtv2 = PsbtV2.fromV0(psbtv0.toBuffer(), true);
|
|
96
|
+
const buffer = psbtv2.serialize();
|
|
97
|
+
const version = PsbtV2.getPsbtVersionNumber(buffer);
|
|
98
|
+
|
|
99
|
+
expect(version).toBe(2);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("should handle the version field from the test PSBT", () => {
|
|
103
|
+
const psbtBuf = Buffer.from(
|
|
104
|
+
"cHNidP8BAAoBAAAAAAAAAAAAAQIEAgAAAAEDBAAAAAABBAECAQUBAgH7BAIAAAAAAQBxAgAAAAGTarLgEHL3k8/kyXdU3hth/gPn22U2yLLyHdC1dCxIRQEAAAAA/v///wLe4ccAAAAAABYAFOt418QL8QY7Dj/OKcNWW2ichVmrECcAAAAAAAAWABQjGNZvhP71xIdfkzsDjcY4MfjaE/mXHgABAR8QJwAAAAAAABYAFCMY1m+E/vXEh1+TOwONxjgx+NoTIgYDRV7nztyXsLpDW4AGb8ksljo0xgAxeYHRNTMMTuQ6x6MY9azC/VQAAIABAACAAAAAgAAAAAABAAAAAQ4gniz+J/Cth7eKI31ddAXUowZmyjYdWFpGew3+QiYrTbQBDwQBAAAAARAE/f///wESBAAAAAAAAQBxAQAAAAEORx706Sway1HvyGYPjT9pk26pybK/9y/5vIHFHvz0ZAEAAAAAAAAAAAJgrgoAAAAAABYAFDXG4N1tPISxa6iF3Kc6yGPQtZPsrwYyAAAAAAAWABTcKG4M0ua9N86+nsNJ+18IkFZy/AAAAAABAR9grgoAAAAAABYAFDXG4N1tPISxa6iF3Kc6yGPQtZPsIgYCcbW3ea2HCDhYd5e89vDHrsWr52pwnXJPSNLibPh08KAY9azC/VQAAIABAACAAAAAgAEAAAAAAAAAAQ4gr7+uBlkPdB/xr1m2rEYRJjNqTEqC21U99v76tzesM/MBDwQAAAAAARAE/f///wESBAAAAAAAIgICKexHcnEx7SWIogxG7amrt9qm9J/VC6/nC5xappYcTswY9azC/VQAAIABAACAAAAAgAEAAAAKAAAAAQMIqDoGAAAAAAABBBYAFOs4+puBKPgfJule2wxf+uqDaQ/kAAEDCOCTBAAAAAAAAQQiACA/qWbJ3c3C/ZbkpeG8dlufr2zos+tPEQSq1r33cyTlvgA=",
|
|
105
|
+
"base64",
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
const version = PsbtV2.getPsbtVersionNumber(psbtBuf);
|
|
109
|
+
expect(version).toBe(2);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("should return 0 when PSBTv2 has no explicit VERSION field", () => {
|
|
113
|
+
const psbtv2 = new PsbtV2();
|
|
114
|
+
const buffer = psbtv2.serialize();
|
|
115
|
+
|
|
116
|
+
const version = PsbtV2.getPsbtVersionNumber(buffer);
|
|
117
|
+
|
|
118
|
+
expect(version).toBe(0);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("should handle global keys with key data and empty values", () => {
|
|
122
|
+
const magic = Buffer.from([0x70, 0x73, 0x62, 0x74, 0xff]);
|
|
123
|
+
|
|
124
|
+
// Manually craft a minimal PSBT-like buffer:
|
|
125
|
+
// - One global key with keyLen = 2 (type + 1 byte keyData)
|
|
126
|
+
// - valueLen = 0 (empty value)
|
|
127
|
+
// - followed by keyLen = 0 as global-map terminator
|
|
128
|
+
const body = Buffer.from([
|
|
129
|
+
0x02, // keyLen = 2
|
|
130
|
+
psbtGlobal.TX_VERSION, // keyType
|
|
131
|
+
0x99, // keyData (ignored by version parser)
|
|
132
|
+
0x00, // valueLen = 0
|
|
133
|
+
0x00, // next keyLen = 0 -> end of global map
|
|
134
|
+
]);
|
|
135
|
+
|
|
136
|
+
const buffer = Buffer.concat([magic, body]);
|
|
137
|
+
|
|
138
|
+
const version = PsbtV2.getPsbtVersionNumber(buffer);
|
|
139
|
+
|
|
140
|
+
expect(version).toBe(0);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe("optional getters and taproot helpers", () => {
|
|
145
|
+
it("should return undefined for optional globals and inputs when not set", () => {
|
|
146
|
+
const psbtv2 = new PsbtV2();
|
|
147
|
+
|
|
148
|
+
expect(psbtv2.getGlobalFallbackLocktime()).toBeUndefined();
|
|
149
|
+
|
|
150
|
+
// Ensure input map exists but no optional fields are set
|
|
151
|
+
psbtv2.setInputSequence(0, 0xfffffffe);
|
|
152
|
+
|
|
153
|
+
expect(psbtv2.getInputSighashType(0)).toBeUndefined();
|
|
154
|
+
expect(psbtv2.getInputTapKeySig(0)).toBeUndefined();
|
|
155
|
+
|
|
156
|
+
const witnessUtxo = psbtv2.getInputWitnessUtxo(0);
|
|
157
|
+
expect(witnessUtxo).toBeUndefined();
|
|
158
|
+
|
|
159
|
+
const bip32Deriv = psbtv2.getInputBip32Derivation(0, Buffer.alloc(33, 2));
|
|
160
|
+
expect(bip32Deriv).toBeUndefined();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("should encode and decode taproot BIP32 derivation for inputs and outputs", () => {
|
|
164
|
+
const psbtv2 = new PsbtV2();
|
|
165
|
+
|
|
166
|
+
const pubkey = Buffer.alloc(32, 1);
|
|
167
|
+
const hashes = [Buffer.alloc(32, 2), Buffer.alloc(32, 3)];
|
|
168
|
+
const masterFingerprint = Buffer.from("01020304", "hex");
|
|
169
|
+
const path = [0x8000002c, 0x80000001, 0x80000000, 0, 5];
|
|
170
|
+
|
|
171
|
+
psbtv2.setInputTapBip32Derivation(0, pubkey, hashes, masterFingerprint, path);
|
|
172
|
+
psbtv2.setOutputTapBip32Derivation(0, pubkey, hashes, masterFingerprint, path);
|
|
173
|
+
|
|
174
|
+
const inputDeriv = psbtv2.getInputTapBip32Derivation(0, pubkey);
|
|
175
|
+
expect(inputDeriv.hashes).toHaveLength(2);
|
|
176
|
+
expect(inputDeriv.hashes[0]).toEqual(hashes[0]);
|
|
177
|
+
expect(inputDeriv.hashes[1]).toEqual(hashes[1]);
|
|
178
|
+
expect(inputDeriv.masterFingerprint).toEqual(masterFingerprint);
|
|
179
|
+
expect(inputDeriv.path).toEqual(path);
|
|
180
|
+
|
|
181
|
+
const outputDeriv = psbtv2.getOutputTapBip32Derivation(0, pubkey);
|
|
182
|
+
expect(outputDeriv.hashes).toHaveLength(2);
|
|
183
|
+
expect(outputDeriv.hashes[0]).toEqual(hashes[0]);
|
|
184
|
+
expect(outputDeriv.hashes[1]).toEqual(hashes[1]);
|
|
185
|
+
expect(outputDeriv.masterFingerprint).toEqual(masterFingerprint);
|
|
186
|
+
expect(outputDeriv.path).toEqual(path);
|
|
187
|
+
|
|
188
|
+
const keyDatas = psbtv2.getInputKeyDatas(0, psbtIn.TAP_BIP32_DERIVATION);
|
|
189
|
+
expect(keyDatas).toHaveLength(1);
|
|
190
|
+
expect(keyDatas[0]).toEqual(pubkey);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("should delete specified input entries and throw when accessing them afterwards", () => {
|
|
194
|
+
const psbtv2 = new PsbtV2();
|
|
195
|
+
|
|
196
|
+
const pubkey = Buffer.alloc(32, 9);
|
|
197
|
+
const hashes = [Buffer.alloc(32, 4)];
|
|
198
|
+
const masterFingerprint = Buffer.from("0a0b0c0d", "hex");
|
|
199
|
+
const path = [0x8000002c, 0x80000001, 0x80000000, 1, 0];
|
|
200
|
+
|
|
201
|
+
psbtv2.setInputTapBip32Derivation(0, pubkey, hashes, masterFingerprint, path);
|
|
202
|
+
|
|
203
|
+
expect(() => psbtv2.getInputTapBip32Derivation(0, pubkey)).not.toThrow();
|
|
204
|
+
|
|
205
|
+
psbtv2.deleteInputEntries(0, [psbtIn.TAP_BIP32_DERIVATION]);
|
|
206
|
+
|
|
207
|
+
expect(() => psbtv2.getInputTapBip32Derivation(0, pubkey)).toThrow(NoSuchEntry);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("should set and get txModifiable global flag", () => {
|
|
211
|
+
const psbtv2 = new PsbtV2();
|
|
212
|
+
|
|
213
|
+
expect(psbtv2.getGlobalTxModifiable()).toBeUndefined();
|
|
214
|
+
|
|
215
|
+
const flag = Buffer.from([0x01]);
|
|
216
|
+
psbtv2.setGlobalTxModifiable(flag);
|
|
217
|
+
|
|
218
|
+
expect(psbtv2.getGlobalTxModifiable()).toEqual(flag);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("should default input sequence to 0xffffffff when not set", () => {
|
|
222
|
+
const psbtv2 = new PsbtV2();
|
|
223
|
+
|
|
224
|
+
const prevTxId = Buffer.alloc(32, 0);
|
|
225
|
+
psbtv2.setInputPreviousTxId(0, prevTxId);
|
|
226
|
+
|
|
227
|
+
expect(psbtv2.getInputSequence(0)).toBe(0xffffffff);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("should throw when accessing required input data for a non-existent map", () => {
|
|
231
|
+
const psbtv2 = new PsbtV2();
|
|
232
|
+
|
|
233
|
+
expect(() => psbtv2.getInputFinalScriptwitness(0)).toThrow("No such map");
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it("should support taproot derivation with empty hashes vector", () => {
|
|
237
|
+
const psbtv2 = new PsbtV2();
|
|
238
|
+
|
|
239
|
+
const pubkey = Buffer.alloc(32, 7);
|
|
240
|
+
const hashes: Buffer[] = [];
|
|
241
|
+
const masterFingerprint = Buffer.from("01020304", "hex");
|
|
242
|
+
const path = [0x8000002c, 0x80000001];
|
|
243
|
+
|
|
244
|
+
psbtv2.setInputTapBip32Derivation(0, pubkey, hashes, masterFingerprint, path);
|
|
245
|
+
|
|
246
|
+
const deriv = psbtv2.getInputTapBip32Derivation(0, pubkey);
|
|
247
|
+
expect(deriv.hashes).toHaveLength(0);
|
|
248
|
+
expect(deriv.masterFingerprint).toEqual(masterFingerprint);
|
|
249
|
+
expect(deriv.path).toEqual(path);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it("should validate BIP32 input pubkey length", () => {
|
|
253
|
+
const psbtv2 = new PsbtV2();
|
|
254
|
+
|
|
255
|
+
const invalidPubkey = Buffer.alloc(32, 1); // must be 33 bytes
|
|
256
|
+
const masterFingerprint = Buffer.from("01020304", "hex");
|
|
257
|
+
|
|
258
|
+
expect(() =>
|
|
259
|
+
psbtv2.setInputBip32Derivation(0, invalidPubkey, masterFingerprint, [0x8000002c]),
|
|
260
|
+
).toThrow("Invalid pubkey length: 32");
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it("should validate taproot BIP32 pubkey length", () => {
|
|
264
|
+
const psbtv2 = new PsbtV2();
|
|
265
|
+
|
|
266
|
+
const invalidPubkey = Buffer.alloc(33, 1); // must be 32 bytes
|
|
267
|
+
const masterFingerprint = Buffer.from("01020304", "hex");
|
|
268
|
+
|
|
269
|
+
expect(() =>
|
|
270
|
+
psbtv2.setInputTapBip32Derivation(0, invalidPubkey, [], masterFingerprint, [0]),
|
|
271
|
+
).toThrow("Invalid pubkey length: 33");
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it("should parse and validate BIP32 string paths", () => {
|
|
275
|
+
const validPath = "m/84'/1'/0'/0/0";
|
|
276
|
+
expect(parseBip32Path(validPath)).toEqual([
|
|
277
|
+
0x80000054, // 84'
|
|
278
|
+
0x80000001, // 1'
|
|
279
|
+
0x80000000, // 0'
|
|
280
|
+
0, // 0
|
|
281
|
+
0, // 0
|
|
282
|
+
]);
|
|
283
|
+
|
|
284
|
+
const invalidPath = "m/84'/1'/foo/0/0";
|
|
285
|
+
expect(() => parseBip32Path(invalidPath)).toThrow(/Invalid BIP32 path segment/);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it("should set and get taproot key signature", () => {
|
|
289
|
+
const psbtv2 = new PsbtV2();
|
|
290
|
+
|
|
291
|
+
const sig = Buffer.from("aa", "hex");
|
|
292
|
+
psbtv2.setInputTapKeySig(0, sig);
|
|
293
|
+
|
|
294
|
+
const storedSig = psbtv2.getInputTapKeySig(0);
|
|
295
|
+
expect(storedSig).toEqual(sig);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it("should set and get output redeem script", () => {
|
|
299
|
+
const psbtv2 = new PsbtV2();
|
|
300
|
+
|
|
301
|
+
const redeemScript = Buffer.from("0014" + "22".repeat(20), "hex");
|
|
302
|
+
|
|
303
|
+
psbtv2.setOutputRedeemScript(0, redeemScript);
|
|
304
|
+
|
|
305
|
+
const outRedeemScript = psbtv2.getOutputRedeemScript(0);
|
|
306
|
+
expect(outRedeemScript).toEqual(redeemScript);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it("should copy psbtv2 state deeply", () => {
|
|
310
|
+
const source = new PsbtV2();
|
|
311
|
+
source.setGlobalTxVersion(2);
|
|
312
|
+
source.setGlobalInputCount(1);
|
|
313
|
+
source.setGlobalOutputCount(1);
|
|
314
|
+
source.setGlobalPsbtVersion(2);
|
|
315
|
+
|
|
316
|
+
const prevTxId = Buffer.alloc(32, 1);
|
|
317
|
+
const script = Buffer.from("0014" + "00".repeat(20), "hex");
|
|
318
|
+
|
|
319
|
+
source.setInputPreviousTxId(0, prevTxId);
|
|
320
|
+
source.setInputSequence(0, 0xfffffffe);
|
|
321
|
+
source.setInputWitnessUtxo(0, Buffer.alloc(8, 1), script);
|
|
322
|
+
|
|
323
|
+
source.setOutputAmount(0, 100000);
|
|
324
|
+
source.setOutputScript(0, script);
|
|
325
|
+
|
|
326
|
+
const target = new PsbtV2();
|
|
327
|
+
source.copy(target);
|
|
328
|
+
|
|
329
|
+
expect(target.getGlobalTxVersion()).toBe(2);
|
|
330
|
+
expect(target.getGlobalInputCount()).toBe(1);
|
|
331
|
+
expect(target.getGlobalOutputCount()).toBe(1);
|
|
332
|
+
expect(target.getGlobalPsbtVersion()).toBe(2);
|
|
333
|
+
|
|
334
|
+
expect(target.getInputPreviousTxid(0)).toEqual(prevTxId);
|
|
335
|
+
expect(target.getInputSequence(0)).toBe(0xfffffffe);
|
|
336
|
+
|
|
337
|
+
const witness = target.getInputWitnessUtxo(0);
|
|
338
|
+
expect(witness).toBeDefined();
|
|
339
|
+
expect(witness!.scriptPubKey).toEqual(script);
|
|
340
|
+
|
|
341
|
+
expect(target.getOutputAmount(0)).toBe(100000);
|
|
342
|
+
expect(target.getOutputScript(0)).toEqual(script);
|
|
343
|
+
|
|
344
|
+
// Mutate source and ensure target is unchanged (deep copy)
|
|
345
|
+
source.setOutputAmount(0, 1);
|
|
346
|
+
expect(target.getOutputAmount(0)).toBe(100000);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it("should throw when deserializing buffer with invalid magic bytes", () => {
|
|
350
|
+
const psbtv2 = new PsbtV2();
|
|
351
|
+
|
|
352
|
+
const invalid = Buffer.alloc(5, 0);
|
|
353
|
+
|
|
354
|
+
expect(() => psbtv2.deserialize(invalid)).toThrow("Invalid magic bytes");
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it("should delete only matching input entries and preserve others", () => {
|
|
358
|
+
const psbtv2 = new PsbtV2();
|
|
359
|
+
|
|
360
|
+
// Add multiple different entry types to input 0
|
|
361
|
+
const pubkey1 = Buffer.alloc(32, 1);
|
|
362
|
+
const pubkey2 = Buffer.alloc(33, 2);
|
|
363
|
+
const hashes = [Buffer.alloc(32, 3)];
|
|
364
|
+
const masterFingerprint = Buffer.from("01020304", "hex");
|
|
365
|
+
const path = [0x8000002c, 0x80000001, 0x80000000, 0, 0];
|
|
366
|
+
|
|
367
|
+
// Set TAP_BIP32_DERIVATION (will be deleted)
|
|
368
|
+
psbtv2.setInputTapBip32Derivation(0, pubkey1, hashes, masterFingerprint, path);
|
|
369
|
+
|
|
370
|
+
// Set BIP32_DERIVATION (will be preserved)
|
|
371
|
+
psbtv2.setInputBip32Derivation(0, pubkey2, masterFingerprint, path);
|
|
372
|
+
|
|
373
|
+
// Set TAP_KEY_SIG (will be deleted)
|
|
374
|
+
psbtv2.setInputTapKeySig(0, Buffer.from("aa", "hex"));
|
|
375
|
+
|
|
376
|
+
// Verify both entries exist before deletion
|
|
377
|
+
expect(() => psbtv2.getInputTapBip32Derivation(0, pubkey1)).not.toThrow();
|
|
378
|
+
expect(() => psbtv2.getInputBip32Derivation(0, pubkey2)).not.toThrow();
|
|
379
|
+
expect(psbtv2.getInputTapKeySig(0)).toBeDefined();
|
|
380
|
+
|
|
381
|
+
// Delete only TAP-related entries
|
|
382
|
+
psbtv2.deleteInputEntries(0, [psbtIn.TAP_BIP32_DERIVATION, psbtIn.TAP_KEY_SIG]);
|
|
383
|
+
|
|
384
|
+
// TAP entries should be gone
|
|
385
|
+
expect(() => psbtv2.getInputTapBip32Derivation(0, pubkey1)).toThrow(NoSuchEntry);
|
|
386
|
+
expect(psbtv2.getInputTapKeySig(0)).toBeUndefined();
|
|
387
|
+
|
|
388
|
+
// BIP32_DERIVATION should still exist
|
|
389
|
+
expect(() => psbtv2.getInputBip32Derivation(0, pubkey2)).not.toThrow();
|
|
390
|
+
const deriv = psbtv2.getInputBip32Derivation(0, pubkey2);
|
|
391
|
+
expect(deriv).toBeDefined();
|
|
392
|
+
expect(deriv!.masterFingerprint).toEqual(masterFingerprint);
|
|
393
|
+
expect(deriv!.path).toEqual(path);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it("should return multiple key datas when multiple entries of same type exist", () => {
|
|
397
|
+
const psbtv2 = new PsbtV2();
|
|
398
|
+
|
|
399
|
+
// Add multiple TAP_BIP32_DERIVATION entries with different pubkeys
|
|
400
|
+
const pubkey1 = Buffer.alloc(32, 1);
|
|
401
|
+
const pubkey2 = Buffer.alloc(32, 2);
|
|
402
|
+
const pubkey3 = Buffer.alloc(32, 3);
|
|
403
|
+
const hashes = [Buffer.alloc(32, 4)];
|
|
404
|
+
const masterFingerprint = Buffer.from("0a0b0c0d", "hex");
|
|
405
|
+
const path = [0x8000002c, 0x80000001, 0x80000000, 0, 0];
|
|
406
|
+
|
|
407
|
+
psbtv2.setInputTapBip32Derivation(0, pubkey1, hashes, masterFingerprint, path);
|
|
408
|
+
psbtv2.setInputTapBip32Derivation(0, pubkey2, hashes, masterFingerprint, path);
|
|
409
|
+
psbtv2.setInputTapBip32Derivation(0, pubkey3, hashes, masterFingerprint, path);
|
|
410
|
+
|
|
411
|
+
// Get all key datas for TAP_BIP32_DERIVATION
|
|
412
|
+
const keyDatas = psbtv2.getInputKeyDatas(0, psbtIn.TAP_BIP32_DERIVATION);
|
|
413
|
+
|
|
414
|
+
expect(keyDatas).toHaveLength(3);
|
|
415
|
+
expect(keyDatas).toContainEqual(pubkey1);
|
|
416
|
+
expect(keyDatas).toContainEqual(pubkey2);
|
|
417
|
+
expect(keyDatas).toContainEqual(pubkey3);
|
|
418
|
+
|
|
419
|
+
// Verify each one can be retrieved
|
|
420
|
+
expect(() => psbtv2.getInputTapBip32Derivation(0, pubkey1)).not.toThrow();
|
|
421
|
+
expect(() => psbtv2.getInputTapBip32Derivation(0, pubkey2)).not.toThrow();
|
|
422
|
+
expect(() => psbtv2.getInputTapBip32Derivation(0, pubkey3)).not.toThrow();
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
it("should return empty array when no keys match the requested type", () => {
|
|
426
|
+
const psbtv2 = new PsbtV2();
|
|
427
|
+
|
|
428
|
+
// Set some input data that's not TAP_BIP32_DERIVATION
|
|
429
|
+
const pubkey = Buffer.alloc(33, 1);
|
|
430
|
+
const masterFingerprint = Buffer.from("01020304", "hex");
|
|
431
|
+
const path = [0x8000002c];
|
|
432
|
+
|
|
433
|
+
psbtv2.setInputBip32Derivation(0, pubkey, masterFingerprint, path);
|
|
434
|
+
|
|
435
|
+
// Try to get TAP_BIP32_DERIVATION keys (should be empty)
|
|
436
|
+
const keyDatas = psbtv2.getInputKeyDatas(0, psbtIn.TAP_BIP32_DERIVATION);
|
|
437
|
+
|
|
438
|
+
expect(keyDatas).toHaveLength(0);
|
|
439
|
+
});
|
|
440
|
+
});
|
|
441
|
+
});
|