-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathparseTransactionWithWalletKeys.ts
More file actions
248 lines (216 loc) · 9.92 KB
/
parseTransactionWithWalletKeys.ts
File metadata and controls
248 lines (216 loc) · 9.92 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
import assert from "node:assert";
import * as utxolib from "@bitgo/utxo-lib";
import { fixedScriptWallet } from "../../js/index.js";
import { BitGoPsbt, InputScriptType } from "../../js/fixedScriptWallet.js";
import { loadPsbtFixture, loadWalletKeysFromFixture, getPsbtBuffer } 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 () {
// Replay protection script that matches Rust tests
const replayProtectionScript = Buffer.from(
"a91420b37094d82a513451ff0ccd9db23aba05bc5ef387",
"hex",
);
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: utxolib.bitgo.RootWalletKeys;
let fixture: ReturnType<typeof loadPsbtFixture>;
before(function () {
fixture = loadPsbtFixture(networkName, "fullsigned");
fullsignedPsbtBytes = getPsbtBuffer(fixture);
bitgoPsbt = fixedScriptWallet.BitGoPsbt.fromBytes(fullsignedPsbtBytes, networkName);
rootWalletKeys = loadWalletKeysFromFixture(networkName);
});
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, {
outputScripts: [replayProtectionScript],
});
// Verify all inputs have addresses and values
parsed.inputs.forEach((input, i) => {
assert.ok(input.address, `Input ${i} should have an address`);
assert.ok(typeof input.value === "bigint", `Input ${i} value should be bigint`);
assert.ok(input.value > 0n, `Input ${i} value should be > 0`);
});
// 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, {
outputScripts: [replayProtectionScript],
});
// 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}`,
);
});
});
it("should fail to parse with other wallet keys", function () {
assert.throws(
() => {
bitgoPsbt.parseTransactionWithWalletKeys(getOtherWalletKeys(), {
outputScripts: [replayProtectionScript],
});
},
(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",
);
});
});
});