Skip to content

Commit c139a64

Browse files
committed
fix: test cases for parseWithdrawPsbt
TICKET: BTC-2545
1 parent 5a87dda commit c139a64

File tree

3 files changed

+641
-75
lines changed

3 files changed

+641
-75
lines changed

modules/abstract-lightning/src/lightning/parseWithdrawPsbt.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -110,10 +110,15 @@ export function verifyChangeAddress(
110110
}).address;
111111
break;
112112
case 86: // P2TR (Taproot)
113-
derivedAddress = utxolib.payments.p2tr({
114-
pubkey: derivedPubkey,
115-
network,
116-
}).address;
113+
// P2TR requires x-only pubkey (32 bytes)
114+
const xOnlyPubkey = derivedPubkey.length === 33 ? derivedPubkey.subarray(1, 33) : derivedPubkey;
115+
derivedAddress = utxolib.payments.p2tr(
116+
{
117+
pubkey: xOnlyPubkey,
118+
network,
119+
},
120+
{ eccLib: utxolib.ecc }
121+
).address;
117122
break;
118123
default:
119124
throw new Error(`Unsupported purpose: ${purpose}`);
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
import * as utxolib from '@bitgo/utxo-lib';
2+
3+
export interface PsbtCreationOptions {
4+
network: utxolib.Network;
5+
inputValue?: number;
6+
outputValue?: number;
7+
outputAddress?: string;
8+
changeValue?: number;
9+
changeDerivationPath?: string;
10+
changePurpose?: 49 | 84 | 86;
11+
includeChangeOutput?: boolean;
12+
masterKey?: utxolib.BIP32Interface; // Optional master key for deriving addresses
13+
}
14+
15+
export interface PsbtCreationResult {
16+
psbt: utxolib.Psbt;
17+
masterKey: utxolib.BIP32Interface;
18+
changeDerivationPath: string;
19+
changePurpose: number;
20+
}
21+
22+
/**
23+
* Creates a random PSBT for testing purposes with customizable options.
24+
* This helper function generates a PSBT with a fake input and configurable outputs.
25+
*
26+
* @param options - Configuration options for PSBT creation
27+
* @returns A constructed PSBT instance and the master key used
28+
*/
29+
export function createRandomPSBT(options: PsbtCreationOptions): PsbtCreationResult {
30+
const {
31+
network,
32+
inputValue = 500000,
33+
outputValue = 100000,
34+
outputAddress,
35+
changeValue,
36+
changeDerivationPath = "m/84'/0'/0'/1/6",
37+
changePurpose = 84,
38+
includeChangeOutput = true,
39+
masterKey,
40+
} = options;
41+
42+
// Use provided master key or generate a new one
43+
const accountMasterKey = masterKey || utxolib.bip32.fromSeed(Buffer.alloc(32, 1), network);
44+
45+
// Generate a new random private key for the input
46+
const inputKeyPair = utxolib.ECPair.makeRandom({ network });
47+
const p2wpkhInput = utxolib.payments.p2wpkh({
48+
pubkey: Buffer.from(inputKeyPair.publicKey),
49+
network,
50+
});
51+
52+
// Create a new PSBT instance
53+
const psbt = new utxolib.Psbt({ network });
54+
55+
// Add a fake input to the PSBT
56+
const fakeTxId = 'ca6852598b48230ac870814b935b0d982d3968eb00a1d97332dceb6cd9b8505e';
57+
const fakeVout = 1;
58+
59+
psbt.addInput({
60+
hash: fakeTxId,
61+
index: fakeVout,
62+
witnessUtxo: {
63+
script: p2wpkhInput.output!,
64+
value: BigInt(inputValue),
65+
},
66+
bip32Derivation: [
67+
{
68+
masterFingerprint: Buffer.alloc(4, 0),
69+
path: "m/84'/0'/0'/0/0",
70+
pubkey: Buffer.from(inputKeyPair.publicKey),
71+
},
72+
],
73+
});
74+
75+
// Add recipient output
76+
let recipientAddress: string;
77+
if (outputAddress) {
78+
recipientAddress = outputAddress;
79+
} else {
80+
// Generate a random recipient address (P2TR for testing)
81+
const recipientKeyPair = utxolib.ECPair.makeRandom({ network });
82+
// P2TR requires x-only pubkey (32 bytes, without the prefix byte)
83+
const xOnlyPubkey =
84+
recipientKeyPair.publicKey.length === 33
85+
? recipientKeyPair.publicKey.subarray(1, 33)
86+
: recipientKeyPair.publicKey;
87+
const recipientP2tr = utxolib.payments.p2tr(
88+
{
89+
pubkey: xOnlyPubkey,
90+
network,
91+
},
92+
{ eccLib: utxolib.ecc }
93+
);
94+
recipientAddress = recipientP2tr.address!;
95+
}
96+
97+
psbt.addOutput({
98+
address: recipientAddress,
99+
value: BigInt(outputValue),
100+
});
101+
102+
// Add change output if requested
103+
if (includeChangeOutput) {
104+
const calculatedChangeValue = changeValue !== undefined ? changeValue : inputValue - outputValue - 10000; // 10k sats fee
105+
106+
// Parse the derivation path to get the change and address indices
107+
// Expected format: m/purpose'/coin_type'/account'/change/address_index
108+
const pathSegments = changeDerivationPath.split('/');
109+
const changeIndex = Number(pathSegments[pathSegments.length - 2]);
110+
const addressIndex = Number(pathSegments[pathSegments.length - 1]);
111+
112+
// Derive the change key from the master key
113+
const changeNode = accountMasterKey.derive(changeIndex).derive(addressIndex);
114+
const changePubkey = changeNode.publicKey;
115+
116+
let changeAddress: string;
117+
let changePayment;
118+
119+
// Create change address based on purpose
120+
switch (changePurpose) {
121+
case 49: // P2SH-P2WPKH
122+
changePayment = utxolib.payments.p2sh({
123+
redeem: utxolib.payments.p2wpkh({
124+
pubkey: changePubkey,
125+
network,
126+
}),
127+
network,
128+
});
129+
changeAddress = changePayment.address!;
130+
break;
131+
case 84: // P2WPKH
132+
changePayment = utxolib.payments.p2wpkh({
133+
pubkey: changePubkey,
134+
network,
135+
});
136+
changeAddress = changePayment.address!;
137+
break;
138+
case 86: // P2TR
139+
const xOnlyChangePubkey = changePubkey.length === 33 ? changePubkey.subarray(1, 33) : changePubkey;
140+
changePayment = utxolib.payments.p2tr(
141+
{
142+
pubkey: xOnlyChangePubkey,
143+
network,
144+
},
145+
{ eccLib: utxolib.ecc }
146+
);
147+
changeAddress = changePayment.address!;
148+
break;
149+
default:
150+
throw new Error(`Unsupported purpose: ${changePurpose}`);
151+
}
152+
153+
psbt.addOutput({
154+
address: changeAddress,
155+
value: BigInt(calculatedChangeValue),
156+
});
157+
158+
// Add bip32Derivation to the change output
159+
psbt.updateOutput(1, {
160+
bip32Derivation: [
161+
{
162+
masterFingerprint: Buffer.alloc(4, 0),
163+
path: changeDerivationPath,
164+
pubkey: changePubkey,
165+
},
166+
],
167+
});
168+
}
169+
170+
return {
171+
psbt,
172+
masterKey: accountMasterKey,
173+
changeDerivationPath,
174+
changePurpose,
175+
};
176+
}

0 commit comments

Comments
 (0)