Skip to content

Commit ddc28a3

Browse files
committed
fix(express): signTransactionV1 type codec
Ticket: WP-6717
1 parent 9ad3b3d commit ddc28a3

File tree

2 files changed

+141
-19
lines changed

2 files changed

+141
-19
lines changed

modules/express/src/typedRoutes/api/v1/signTransaction.ts

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,27 @@ export const signTransactionRequestBody = {
1414
and redeemScript with the same index as the inputs in the
1515
transactionHex */
1616
unspents: t.array(t.any),
17-
/** Keychain containing the xprv to sign with */
18-
keychain: t.intersection([
19-
t.type({
20-
xprv: t.string,
21-
}),
22-
t.record(t.string, t.any),
23-
]),
24-
/** For legacy safe wallets, the private key string */
25-
signingKey: t.string,
17+
/** Keychain containing the xprv to sign with (either this or signingKey required) */
18+
keychain: optional(
19+
t.intersection([
20+
t.type({
21+
xprv: t.string,
22+
}),
23+
t.record(t.string, t.any),
24+
])
25+
),
26+
/** For legacy safe wallets, the private key string (either this or keychain required) */
27+
signingKey: optional(t.string),
2628
/** extra verification of signatures (which are always verified server-side) (defaults to global config) */
2729
validate: optional(t.boolean),
30+
/** PSBT (Partially Signed Bitcoin Transaction) in hex format for PSBT signing flow */
31+
psbt: optional(t.string),
32+
/** Private key in WIF format for single-key fee address */
33+
feeSingleKeyWIF: optional(t.string),
34+
/** Enable Bitcoin Cash signing mode */
35+
forceBCH: optional(t.boolean),
36+
/** Require at least two valid signatures for full local signing */
37+
fullLocalSigning: optional(t.boolean),
2838
};
2939

3040
/**

modules/express/test/unit/typedRoutes/signTransaction.ts

Lines changed: 122 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -131,8 +131,8 @@ describe('SignTransaction codec tests', function () {
131131
});
132132
});
133133

134-
it('should reject body with missing keychain', function () {
135-
const invalidBody = {
134+
it('should accept body with only signingKey (no keychain)', function () {
135+
const validBody = {
136136
transactionHex:
137137
'0100000001c7a6e16e2bcf94fba6e0a5839b7accd93f2d684b4b7d97a75a6c3b9b79644f0c0000000000ffffffff0188130000000000001976a914385ed68a0d08c9d34553774be5ee8d5ce2261fce88ac00000000',
138138
unspents: [
@@ -145,13 +145,15 @@ describe('SignTransaction codec tests', function () {
145145
signingKey: 'L1WKFfxHbVdnqMf6HhNnLhM6hZxewJgBhRbKYoXTQqQaP1oyGZCj',
146146
};
147147

148-
assert.throws(() => {
149-
assertDecode(t.type(signTransactionRequestBody), invalidBody);
150-
});
148+
const decoded = assertDecode(t.type(signTransactionRequestBody), validBody);
149+
assert.strictEqual(decoded.transactionHex, validBody.transactionHex);
150+
assert.deepStrictEqual(decoded.unspents, validBody.unspents);
151+
assert.strictEqual(decoded.signingKey, validBody.signingKey);
152+
assert.strictEqual(decoded.keychain, undefined);
151153
});
152154

153-
it('should reject body with missing signingKey', function () {
154-
const invalidBody = {
155+
it('should accept body with only keychain (no signingKey)', function () {
156+
const validBody = {
155157
transactionHex:
156158
'0100000001c7a6e16e2bcf94fba6e0a5839b7accd93f2d684b4b7d97a75a6c3b9b79644f0c0000000000ffffffff0188130000000000001976a914385ed68a0d08c9d34553774be5ee8d5ce2261fce88ac00000000',
157159
unspents: [
@@ -166,9 +168,11 @@ describe('SignTransaction codec tests', function () {
166168
},
167169
};
168170

169-
assert.throws(() => {
170-
assertDecode(t.type(signTransactionRequestBody), invalidBody);
171-
});
171+
const decoded = assertDecode(t.type(signTransactionRequestBody), validBody);
172+
assert.strictEqual(decoded.transactionHex, validBody.transactionHex);
173+
assert.deepStrictEqual(decoded.unspents, validBody.unspents);
174+
assert.deepStrictEqual(decoded.keychain, validBody.keychain);
175+
assert.strictEqual(decoded.signingKey, undefined);
172176
});
173177

174178
it('should reject body with non-string transactionHex', function () {
@@ -275,6 +279,114 @@ describe('SignTransaction codec tests', function () {
275279
assertDecode(t.type(signTransactionRequestBody), invalidBody);
276280
});
277281
});
282+
283+
it('should accept body with psbt field', function () {
284+
const validBody = {
285+
transactionHex:
286+
'0100000001c7a6e16e2bcf94fba6e0a5839b7accd93f2d684b4b7d97a75a6c3b9b79644f0c0000000000ffffffff0188130000000000001976a914385ed68a0d08c9d34553774be5ee8d5ce2261fce88ac00000000',
287+
unspents: [
288+
{
289+
chainPath: 'm/0/0',
290+
redeemScript:
291+
'522103b31347f19510acbc7f50822ac4093ca80554946c471b43eb937d0c9118d1122d2102cd3787d12af6eb87e7b9af00118a225e2ce663a5c94f555460ae131139a2afee2103bd558669de622fc57a8157f449c52254218dfe4e843f58b214b710c4c36833c153ae',
292+
},
293+
],
294+
keychain: {
295+
xprv: 'xprv9s21ZrQH143K3D8TXfvAJgHVfTEeQNW5Ys9wZtnUZkqPzFzSjbEJrWC1vZ4GnXCvR7rQL2UFX3RSuYeU9MrERm1XBvACow7c36vnz5iYyj2',
296+
},
297+
signingKey: 'L1WKFfxHbVdnqMf6HhNnLhM6hZxewJgBhRbKYoXTQqQaP1oyGZCj',
298+
psbt: '70736274ff01007d0200000001c7a6e16e2bcf94fba6e0a5839b7accd93f2d684b4b7d97a75a6c3b9b79644f0c0000000000ffffffff0188130000000000001976a914385ed68a0d08c9d34553774be5ee8d5ce2261fce88ac000000000001011f00000000000000001976a914385ed68a0d08c9d34553774be5ee8d5ce2261fce88ac00',
299+
};
300+
301+
const decoded = assertDecode(t.type(signTransactionRequestBody), validBody);
302+
assert.strictEqual(decoded.psbt, validBody.psbt);
303+
});
304+
305+
it('should accept body with feeSingleKeyWIF field', function () {
306+
const validBody = {
307+
transactionHex:
308+
'0100000001c7a6e16e2bcf94fba6e0a5839b7accd93f2d684b4b7d97a75a6c3b9b79644f0c0000000000ffffffff0188130000000000001976a914385ed68a0d08c9d34553774be5ee8d5ce2261fce88ac00000000',
309+
unspents: [
310+
{
311+
chainPath: 'm/0/0',
312+
redeemScript:
313+
'522103b31347f19510acbc7f50822ac4093ca80554946c471b43eb937d0c9118d1122d2102cd3787d12af6eb87e7b9af00118a225e2ce663a5c94f555460ae131139a2afee2103bd558669de622fc57a8157f449c52254218dfe4e843f58b214b710c4c36833c153ae',
314+
},
315+
],
316+
keychain: {
317+
xprv: 'xprv9s21ZrQH143K3D8TXfvAJgHVfTEeQNW5Ys9wZtnUZkqPzFzSjbEJrWC1vZ4GnXCvR7rQL2UFX3RSuYeU9MrERm1XBvACow7c36vnz5iYyj2',
318+
},
319+
signingKey: 'L1WKFfxHbVdnqMf6HhNnLhM6hZxewJgBhRbKYoXTQqQaP1oyGZCj',
320+
feeSingleKeyWIF: 'L3VpKvNZWWzZUmzJQVnxPWJhXvdXLKhfDxJNVZMvYpZPvGBBXGJN',
321+
};
322+
323+
const decoded = assertDecode(t.type(signTransactionRequestBody), validBody);
324+
assert.strictEqual(decoded.feeSingleKeyWIF, validBody.feeSingleKeyWIF);
325+
});
326+
327+
it('should accept body with forceBCH field', function () {
328+
const validBody = {
329+
transactionHex:
330+
'0100000001c7a6e16e2bcf94fba6e0a5839b7accd93f2d684b4b7d97a75a6c3b9b79644f0c0000000000ffffffff0188130000000000001976a914385ed68a0d08c9d34553774be5ee8d5ce2261fce88ac00000000',
331+
unspents: [
332+
{
333+
chainPath: 'm/0/0',
334+
redeemScript:
335+
'522103b31347f19510acbc7f50822ac4093ca80554946c471b43eb937d0c9118d1122d2102cd3787d12af6eb87e7b9af00118a225e2ce663a5c94f555460ae131139a2afee2103bd558669de622fc57a8157f449c52254218dfe4e843f58b214b710c4c36833c153ae',
336+
},
337+
],
338+
keychain: {
339+
xprv: 'xprv9s21ZrQH143K3D8TXfvAJgHVfTEeQNW5Ys9wZtnUZkqPzFzSjbEJrWC1vZ4GnXCvR7rQL2UFX3RSuYeU9MrERm1XBvACow7c36vnz5iYyj2',
340+
},
341+
signingKey: 'L1WKFfxHbVdnqMf6HhNnLhM6hZxewJgBhRbKYoXTQqQaP1oyGZCj',
342+
forceBCH: true,
343+
};
344+
345+
const decoded = assertDecode(t.type(signTransactionRequestBody), validBody);
346+
assert.strictEqual(decoded.forceBCH, validBody.forceBCH);
347+
});
348+
349+
it('should accept body with fullLocalSigning field', function () {
350+
const validBody = {
351+
transactionHex:
352+
'0100000001c7a6e16e2bcf94fba6e0a5839b7accd93f2d684b4b7d97a75a6c3b9b79644f0c0000000000ffffffff0188130000000000001976a914385ed68a0d08c9d34553774be5ee8d5ce2261fce88ac00000000',
353+
unspents: [
354+
{
355+
chainPath: 'm/0/0',
356+
redeemScript:
357+
'522103b31347f19510acbc7f50822ac4093ca80554946c471b43eb937d0c9118d1122d2102cd3787d12af6eb87e7b9af00118a225e2ce663a5c94f555460ae131139a2afee2103bd558669de622fc57a8157f449c52254218dfe4e843f58b214b710c4c36833c153ae',
358+
},
359+
],
360+
keychain: {
361+
xprv: 'xprv9s21ZrQH143K3D8TXfvAJgHVfTEeQNW5Ys9wZtnUZkqPzFzSjbEJrWC1vZ4GnXCvR7rQL2UFX3RSuYeU9MrERm1XBvACow7c36vnz5iYyj2',
362+
},
363+
signingKey: 'L1WKFfxHbVdnqMf6HhNnLhM6hZxewJgBhRbKYoXTQqQaP1oyGZCj',
364+
fullLocalSigning: true,
365+
};
366+
367+
const decoded = assertDecode(t.type(signTransactionRequestBody), validBody);
368+
assert.strictEqual(decoded.fullLocalSigning, validBody.fullLocalSigning);
369+
});
370+
371+
it('should reject body with neither keychain nor signingKey', function () {
372+
const invalidBody = {
373+
transactionHex:
374+
'0100000001c7a6e16e2bcf94fba6e0a5839b7accd93f2d684b4b7d97a75a6c3b9b79644f0c0000000000ffffffff0188130000000000001976a914385ed68a0d08c9d34553774be5ee8d5ce2261fce88ac00000000',
375+
unspents: [
376+
{
377+
chainPath: 'm/0/0',
378+
redeemScript:
379+
'522103b31347f19510acbc7f50822ac4093ca80554946c471b43eb937d0c9118d1122d2102cd3787d12af6eb87e7b9af00118a225e2ce663a5c94f555460ae131139a2afee2103bd558669de622fc57a8157f449c52254218dfe4e843f58b214b710c4c36833c153ae',
380+
},
381+
],
382+
};
383+
384+
// Note: io-ts will accept this (both are optional), but the handler/SDK will reject it
385+
// This test documents that the codec validation passes, but runtime validation will fail
386+
const decoded = assertDecode(t.type(signTransactionRequestBody), invalidBody);
387+
assert.strictEqual(decoded.keychain, undefined);
388+
assert.strictEqual(decoded.signingKey, undefined);
389+
});
278390
});
279391

280392
describe('Edge cases', function () {

0 commit comments

Comments
 (0)