-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathparseTransactionWithWalletKeys.ts
More file actions
338 lines (301 loc) · 13.4 KB
/
parseTransactionWithWalletKeys.ts
File metadata and controls
338 lines (301 loc) · 13.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
import assert from "node:assert";
import * as utxolib from "@bitgo/utxo-lib";
import { fixedScriptWallet } from "../../js/index.js";
import { BitGoPsbt, InputScriptType } from "../../js/fixedScriptWallet/index.js";
import type { RootWalletKeys } from "../../js/fixedScriptWallet/RootWalletKeys.js";
import type { ECPair } from "../../js/index.js";
import {
loadPsbtFixture,
loadWalletKeysFromFixture,
getPsbtBuffer,
loadReplayProtectionKeyFromFixture,
type Fixture,
} from "./fixtureUtil.js";
function getExpectedInputScriptType(fixtureScriptType: string): InputScriptType {
// Map fixture types to InputScriptType values
// Based on the Rust mapping in src/fixed_script_wallet/test_utils/fixtures.rs
switch (fixtureScriptType) {
case "p2shP2pk":
case "p2sh":
case "p2shP2wsh":
case "p2wsh":
return fixtureScriptType;
case "p2tr":
return "p2trLegacy";
case "p2trMusig2":
return "p2trMusig2ScriptPath";
case "taprootKeyPathSpend":
return "p2trMusig2KeyPath";
default:
throw new Error(`Unknown fixture script type: ${fixtureScriptType}`);
}
}
function getOtherWalletKeys(): utxolib.bitgo.RootWalletKeys {
const otherWalletKeys = utxolib.testutil.getKeyTriple("too many secrets");
return new utxolib.bitgo.RootWalletKeys(otherWalletKeys);
}
describe("parseTransactionWithWalletKeys", function () {
const supportedNetworks = utxolib.getNetworkList().filter((network) => {
return (
utxolib.isMainnet(network) &&
network !== utxolib.networks.bitcoincash &&
network !== utxolib.networks.bitcoingold &&
network !== utxolib.networks.bitcoinsv &&
network !== utxolib.networks.ecash &&
network !== utxolib.networks.zcash
);
});
supportedNetworks.forEach((network) => {
const networkName = utxolib.getNetworkName(network);
describe(`network: ${networkName}`, function () {
let fullsignedPsbtBytes: Buffer;
let bitgoPsbt: BitGoPsbt;
let rootWalletKeys: RootWalletKeys;
let replayProtectionKey: ECPair;
let fixture: Fixture;
before(function () {
fixture = loadPsbtFixture(networkName, "fullsigned");
fullsignedPsbtBytes = getPsbtBuffer(fixture);
bitgoPsbt = fixedScriptWallet.BitGoPsbt.fromBytes(fullsignedPsbtBytes, networkName);
rootWalletKeys = loadWalletKeysFromFixture(fixture);
replayProtectionKey = loadReplayProtectionKeyFromFixture(fixture);
});
it("should have matching unsigned transaction ID", function () {
const unsignedTxid = bitgoPsbt.unsignedTxid();
const expectedUnsignedTxid = utxolib.bitgo
.createPsbtFromBuffer(fullsignedPsbtBytes, network)
.getUnsignedTx()
.getId();
assert.strictEqual(unsignedTxid, expectedUnsignedTxid);
});
it("should parse transaction and identify internal/external outputs", function () {
const parsed = bitgoPsbt.parseTransactionWithWalletKeys(rootWalletKeys, {
publicKeys: [replayProtectionKey],
});
// Verify all inputs have addresses and values
parsed.inputs.forEach((input, i) => {
// Verify previousOutput structure
assert.ok(input.previousOutput, `Input ${i} should have previousOutput`);
assert.ok(
typeof input.previousOutput === "object",
`Input ${i} previousOutput should be an object`,
);
assert.ok(
typeof input.previousOutput.txid === "string",
`Input ${i} previousOutput.txid should be string`,
);
assert.strictEqual(
input.previousOutput.txid.length,
64,
`Input ${i} previousOutput.txid should be 64 chars (32 bytes hex)`,
);
assert.ok(
typeof input.previousOutput.vout === "number",
`Input ${i} previousOutput.vout should be number`,
);
assert.ok(
input.previousOutput.vout >= 0,
`Input ${i} previousOutput.vout should be >= 0`,
);
// Verify address
assert.ok(input.address, `Input ${i} should have an address`);
assert.ok(typeof input.address === "string", `Input ${i} address should be string`);
// Verify value
assert.ok(typeof input.value === "bigint", `Input ${i} value should be bigint`);
assert.ok(input.value > 0n, `Input ${i} value should be > 0`);
// Verify scriptId structure (can be null for replay protection inputs)
if (input.scriptId !== null) {
assert.ok(
typeof input.scriptId === "object",
`Input ${i} scriptId should be an object when present`,
);
assert.ok(
typeof input.scriptId.chain === "number",
`Input ${i} scriptId.chain should be number`,
);
assert.ok(
typeof input.scriptId.index === "number",
`Input ${i} scriptId.index should be number`,
);
assert.ok(input.scriptId.chain >= 0, `Input ${i} scriptId.chain should be >= 0`);
assert.ok(input.scriptId.index >= 0, `Input ${i} scriptId.index should be >= 0`);
}
// Verify scriptType is present
assert.ok(input.scriptType, `Input ${i} should have scriptType`);
assert.ok(typeof input.scriptType === "string", `Input ${i} scriptType should be string`);
});
// Validate outputs
assert.ok(parsed.outputs.length > 0, "Should have at least one output");
// Count internal outputs (scriptId is defined and not null)
const internalOutputs = parsed.outputs.filter((o) => o.scriptId);
// Count external outputs (scriptId is null or undefined)
const externalOutputs = parsed.outputs.filter((o) => o.scriptId === null);
assert.ok(externalOutputs.every((o) => o.address || o.script));
const nonAddressOutputs = externalOutputs.filter((o) => o.address === null);
assert.strictEqual(nonAddressOutputs.length, 1);
const [opReturnOutput] = nonAddressOutputs;
const expectedOpReturn = utxolib.payments.embed({
data: [Buffer.from("setec astronomy")],
}).output;
assert.strictEqual(
Buffer.from(opReturnOutput.script).toString("hex"),
expectedOpReturn.toString("hex"),
);
// Fixtures now have 3 external outputs
assert.ok(internalOutputs.length > 0, "Should have internal outputs (have scriptId)");
assert.strictEqual(
externalOutputs.length,
3,
"Should have 3 external outputs in test fixture",
);
// Verify all outputs have proper structure
parsed.outputs.forEach((output, i) => {
assert.ok(output.script instanceof Uint8Array, `Output ${i} script should be Uint8Array`);
assert.ok(typeof output.value === "bigint", `Output ${i} value should be bigint`);
assert.ok(output.value > 0n, `Output ${i} value should be > 0`);
// Address is optional for non-standard scripts
});
// Verify spend amount (should be > 0 since there are external outputs)
assert.strictEqual(parsed.spendAmount, 900n * 3n);
// Verify miner fee calculation
const totalInputValue = parsed.inputs.reduce((sum, i) => sum + i.value, 0n);
const totalOutputValue = parsed.outputs.reduce((sum, o) => sum + o.value, 0n);
assert.strictEqual(
parsed.minerFee,
totalInputValue - totalOutputValue,
"Miner fee should equal inputs minus outputs",
);
assert.ok(parsed.minerFee > 0n, "Miner fee should be > 0");
// Verify virtual size
assert.ok(typeof parsed.virtualSize === "number", "Virtual size should be a number");
assert.ok(parsed.virtualSize > 0, "Virtual size should be > 0");
});
it("should parse inputs with correct scriptType", function () {
const parsed = bitgoPsbt.parseTransactionWithWalletKeys(rootWalletKeys, {
publicKeys: [replayProtectionKey],
});
// Verify all inputs have scriptType matching fixture
parsed.inputs.forEach((input, i) => {
const fixtureInput = fixture.psbtInputs[i];
const expectedScriptType = getExpectedInputScriptType(fixtureInput.type);
assert.strictEqual(
input.scriptType,
expectedScriptType,
`Input ${i} scriptType should be ${expectedScriptType}, got ${input.scriptType}`,
);
// Verify previousOutput is present and structured correctly
assert.ok(input.previousOutput, `Input ${i} should have previousOutput`);
assert.ok(
typeof input.previousOutput === "object",
`Input ${i} previousOutput should be an object`,
);
assert.ok(
typeof input.previousOutput.txid === "string",
`Input ${i} previousOutput.txid should be string`,
);
assert.strictEqual(
input.previousOutput.txid.length,
64,
`Input ${i} previousOutput.txid should be 64 chars`,
);
assert.ok(
typeof input.previousOutput.vout === "number",
`Input ${i} previousOutput.vout should be number`,
);
// Verify scriptId structure when present (can be null for replay protection inputs)
if (input.scriptId !== null) {
assert.ok(
typeof input.scriptId === "object",
`Input ${i} scriptId should be an object when present`,
);
assert.ok(
typeof input.scriptId.chain === "number",
`Input ${i} scriptId.chain should be number`,
);
assert.ok(
typeof input.scriptId.index === "number",
`Input ${i} scriptId.index should be number`,
);
}
});
});
it("should fail to parse with other wallet keys", function () {
assert.throws(
() => {
bitgoPsbt.parseTransactionWithWalletKeys(getOtherWalletKeys(), {
publicKeys: [replayProtectionKey],
});
},
(error: Error) => {
return error.message.includes(
"Failed to parse transaction: Input 0: wallet validation failed",
);
},
);
});
it("should recognize output for other wallet keys", function () {
const parsedOutputs = bitgoPsbt.parseOutputsWithWalletKeys(getOtherWalletKeys());
// Should return an array of parsed outputs
assert.ok(Array.isArray(parsedOutputs), "Should return an array");
assert.ok(parsedOutputs.length > 0, "Should have at least one output");
// Verify all outputs have proper structure
parsedOutputs.forEach((output, i) => {
assert.ok(output.script instanceof Uint8Array, `Output ${i} script should be Uint8Array`);
assert.ok(typeof output.value === "bigint", `Output ${i} value should be bigint`);
assert.ok(output.value > 0n, `Output ${i} value should be > 0`);
// Address can be null for non-standard scripts
assert.ok(
typeof output.address === "string" || output.address === null,
`Output ${i} address should be string or null`,
);
// scriptId can be null for external outputs
assert.ok(
output.scriptId === null ||
(typeof output.scriptId === "object" &&
typeof output.scriptId.chain === "number" &&
typeof output.scriptId.index === "number"),
`Output ${i} scriptId should be null or an object with chain and index`,
);
});
// Compare with the original wallet keys to verify we get different results
const originalParsedOutputs = bitgoPsbt.parseOutputsWithWalletKeys(rootWalletKeys);
// Should have the same number of outputs
assert.strictEqual(
parsedOutputs.length,
originalParsedOutputs.length,
"Should parse the same number of outputs",
);
// Find outputs that belong to the other wallet keys (scriptId !== null)
const otherWalletOutputs = parsedOutputs.filter((o) => o.scriptId !== null);
// Should have exactly one output for the other wallet keys
assert.strictEqual(
otherWalletOutputs.length,
1,
"Should have exactly one output belonging to the other wallet keys",
);
// Verify that this output is marked as external (scriptId === null) under regular wallet keys
const otherWalletOutputIndex = parsedOutputs.findIndex((o) => o.scriptId !== null);
const sameOutputWithRegularKeys = originalParsedOutputs[otherWalletOutputIndex];
assert.strictEqual(
sameOutputWithRegularKeys.scriptId,
null,
"The output belonging to other wallet keys should be marked as external (scriptId === null) when parsed with regular wallet keys",
);
});
});
});
describe("error handling", function () {
it("should throw error for invalid PSBT bytes", function () {
const invalidBytes = new Uint8Array([0x00, 0x01, 0x02]);
assert.throws(
() => {
fixedScriptWallet.BitGoPsbt.fromBytes(invalidBytes, "bitcoin");
},
(error: Error) => {
return error.message.includes("Failed to deserialize PSBT");
},
"Should throw error for invalid PSBT bytes",
);
});
});
});