Skip to content

Commit 83af8e1

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

File tree

3 files changed

+640
-75
lines changed

3 files changed

+640
-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: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
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 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 createTestPsbt(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+
const fixedSeed = Buffer.from('0101010101010101010101010101010101010101010101010101010101010101', 'hex');
42+
const accountMasterKey = masterKey || utxolib.bip32.fromSeed(fixedSeed, network);
43+
44+
const inputPrivateKey = Buffer.from('0202020202020202020202020202020202020202020202020202020202020202', 'hex');
45+
const inputKeyPair = utxolib.ECPair.fromPrivateKey(inputPrivateKey, { network });
46+
const p2wpkhInput = utxolib.payments.p2wpkh({
47+
pubkey: Buffer.from(inputKeyPair.publicKey),
48+
network,
49+
});
50+
51+
// Create a new PSBT instance
52+
const psbt = new utxolib.Psbt({ network });
53+
54+
// Add a fake input to the PSBT
55+
const fakeTxId = 'ca6852598b48230ac870814b935b0d982d3968eb00a1d97332dceb6cd9b8505e';
56+
const fakeVout = 1;
57+
58+
psbt.addInput({
59+
hash: fakeTxId,
60+
index: fakeVout,
61+
witnessUtxo: {
62+
script: p2wpkhInput.output!,
63+
value: BigInt(inputValue),
64+
},
65+
bip32Derivation: [
66+
{
67+
masterFingerprint: Buffer.alloc(4, 0),
68+
path: "m/84'/0'/0'/0/0",
69+
pubkey: Buffer.from(inputKeyPair.publicKey),
70+
},
71+
],
72+
});
73+
74+
// Add recipient output
75+
let recipientAddress: string;
76+
if (outputAddress) {
77+
recipientAddress = outputAddress;
78+
} else {
79+
const recipientPrivateKey = Buffer.from('0303030303030303030303030303030303030303030303030303030303030303', 'hex');
80+
const recipientKeyPair = utxolib.ECPair.fromPrivateKey(recipientPrivateKey, { network });
81+
// P2TR requires x-only pubkey (32 bytes, without the prefix byte)
82+
const xOnlyPubkey =
83+
recipientKeyPair.publicKey.length === 33
84+
? recipientKeyPair.publicKey.subarray(1, 33)
85+
: recipientKeyPair.publicKey;
86+
const recipientP2tr = utxolib.payments.p2tr(
87+
{
88+
pubkey: xOnlyPubkey,
89+
network,
90+
},
91+
{ eccLib: utxolib.ecc }
92+
);
93+
recipientAddress = recipientP2tr.address!;
94+
}
95+
96+
psbt.addOutput({
97+
address: recipientAddress,
98+
value: BigInt(outputValue),
99+
});
100+
101+
// Add change output if requested
102+
if (includeChangeOutput) {
103+
const calculatedChangeValue = changeValue !== undefined ? changeValue : inputValue - outputValue - 10000; // 10k sats fee
104+
105+
// Parse the derivation path to get the change and address indices
106+
// Expected format: m/purpose'/coin_type'/account'/change/address_index
107+
const pathSegments = changeDerivationPath.split('/');
108+
const changeIndex = Number(pathSegments[pathSegments.length - 2]);
109+
const addressIndex = Number(pathSegments[pathSegments.length - 1]);
110+
111+
// Derive the change key from the master key
112+
const changeNode = accountMasterKey.derive(changeIndex).derive(addressIndex);
113+
const changePubkey = changeNode.publicKey;
114+
115+
let changeAddress: string;
116+
let changePayment;
117+
118+
// Create change address based on purpose
119+
switch (changePurpose) {
120+
case 49: // P2SH-P2WPKH
121+
changePayment = utxolib.payments.p2sh({
122+
redeem: utxolib.payments.p2wpkh({
123+
pubkey: changePubkey,
124+
network,
125+
}),
126+
network,
127+
});
128+
changeAddress = changePayment.address!;
129+
break;
130+
case 84: // P2WPKH
131+
changePayment = utxolib.payments.p2wpkh({
132+
pubkey: changePubkey,
133+
network,
134+
});
135+
changeAddress = changePayment.address!;
136+
break;
137+
case 86: // P2TR
138+
const xOnlyChangePubkey = changePubkey.length === 33 ? changePubkey.subarray(1, 33) : changePubkey;
139+
changePayment = utxolib.payments.p2tr(
140+
{
141+
pubkey: xOnlyChangePubkey,
142+
network,
143+
},
144+
{ eccLib: utxolib.ecc }
145+
);
146+
changeAddress = changePayment.address!;
147+
break;
148+
default:
149+
throw new Error(`Unsupported purpose: ${changePurpose}`);
150+
}
151+
152+
psbt.addOutput({
153+
address: changeAddress,
154+
value: BigInt(calculatedChangeValue),
155+
});
156+
157+
// Add bip32Derivation to the change output
158+
psbt.updateOutput(1, {
159+
bip32Derivation: [
160+
{
161+
masterFingerprint: Buffer.alloc(4, 0),
162+
path: changeDerivationPath,
163+
pubkey: changePubkey,
164+
},
165+
],
166+
});
167+
}
168+
169+
return {
170+
psbt,
171+
masterKey: accountMasterKey,
172+
changeDerivationPath,
173+
changePurpose,
174+
};
175+
}

0 commit comments

Comments
 (0)