Skip to content

Commit c851c13

Browse files
Merge pull request #6425 from BitGo/BTC-2143.add-selectTapLeafScript
feat(utxo-core): add selectTapLeafScript option to createPsbt
2 parents 404d96a + b8ecac2 commit c851c13

File tree

7 files changed

+1596
-12
lines changed

7 files changed

+1596
-12
lines changed

modules/utxo-core/src/descriptor/psbt/createPsbt.ts

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
import { TapLeafScript } from 'bip174/src/lib/interfaces';
12
import * as utxolib from '@bitgo/utxo-lib';
2-
import { Descriptor } from '@bitgo/wasm-miniscript';
3+
import { Descriptor, Miniscript } from '@bitgo/wasm-miniscript';
34

45
import { DerivedDescriptorWalletOutput, WithOptDescriptor } from '../Output';
56
import { Output } from '../../Output';
@@ -13,18 +14,43 @@ import { assertSatisfiable } from './assertSatisfiable';
1314
* */
1415
export const MAX_BIP125_RBF_SEQUENCE = 0xffffffff - 2;
1516

16-
function updateInputsWithDescriptors(psbt: utxolib.bitgo.UtxoPsbt, descriptors: Descriptor[]) {
17-
if (psbt.txInputs.length !== descriptors.length) {
18-
throw new Error(`Input count mismatch (psbt=${psbt.txInputs.length}, descriptors=${descriptors.length})`);
17+
export function findTapLeafScript(input: TapLeafScript[], script: Buffer | Miniscript): TapLeafScript {
18+
if (!Buffer.isBuffer(script)) {
19+
script = Buffer.from(script.encode());
20+
}
21+
const matches = input.filter((leaf) => {
22+
return leaf.script.equals(script);
23+
});
24+
if (matches.length === 0) {
25+
throw new Error(`No tapLeafScript found for script: ${script.toString('hex')}`);
26+
}
27+
if (matches.length > 1) {
28+
throw new Error(`Multiple tapLeafScripts found for script: ${script.toString('hex')}`);
29+
}
30+
return matches[0];
31+
}
32+
33+
function updateInputsWithDescriptors(
34+
psbt: utxolib.bitgo.UtxoPsbt,
35+
inputParams: Array<{ descriptor: Descriptor; selectTapLeafScript?: Miniscript }>
36+
) {
37+
if (psbt.txInputs.length !== inputParams.length) {
38+
throw new Error(`Input count mismatch (psbt=${psbt.txInputs.length}, inputParams=${inputParams.length})`);
1939
}
2040
const wrappedPsbt = toWrappedPsbt(psbt);
21-
for (const [inputIndex, descriptor] of descriptors.entries()) {
22-
assertSatisfiable(psbt, inputIndex, descriptor);
23-
wrappedPsbt.updateInputWithDescriptor(inputIndex, descriptor);
41+
for (const [inputIndex, v] of inputParams.entries()) {
42+
assertSatisfiable(psbt, inputIndex, v.descriptor);
43+
wrappedPsbt.updateInputWithDescriptor(inputIndex, v.descriptor);
2444
}
2545
const unwrappedPsbt = toUtxoPsbt(wrappedPsbt, psbt.network);
2646
for (const inputIndex in psbt.txInputs) {
27-
psbt.data.inputs[inputIndex] = unwrappedPsbt.data.inputs[inputIndex];
47+
const preparedInput = unwrappedPsbt.data.inputs[inputIndex];
48+
const v = inputParams[inputIndex];
49+
if (v.selectTapLeafScript && preparedInput.tapLeafScript) {
50+
const selected = findTapLeafScript(preparedInput.tapLeafScript, v.selectTapLeafScript);
51+
preparedInput.tapLeafScript = [selected];
52+
}
53+
psbt.data.inputs[inputIndex] = preparedInput;
2854
}
2955
}
3056

@@ -49,6 +75,7 @@ export type PsbtParams = {
4975
};
5076

5177
export type DerivedDescriptorTransactionInput = DerivedDescriptorWalletOutput & {
78+
selectTapLeafScript?: Miniscript;
5279
sequence?: number;
5380
};
5481

@@ -64,7 +91,10 @@ export function createPsbt(
6491
psbt.addOutputs(outputs);
6592
updateInputsWithDescriptors(
6693
psbt,
67-
inputs.map((i) => i.descriptor)
94+
inputs.map(({ descriptor, selectTapLeafScript }) => ({
95+
descriptor,
96+
selectTapLeafScript,
97+
}))
6898
);
6999
updateOutputsWithDescriptors(psbt, outputs);
70100
return psbt;

modules/utxo-core/src/testutil/descriptor/descriptors.ts

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import assert from 'assert';
22

3-
import { Descriptor, ast } from '@bitgo/wasm-miniscript';
3+
import { Miniscript, Descriptor, ast } from '@bitgo/wasm-miniscript';
44
import { bip32, BIP32Interface } from '@bitgo/utxo-lib';
55

66
import { DescriptorMap, PsbtParams } from '../../descriptor';
@@ -134,6 +134,50 @@ function getDescriptorNode(
134134
throw new Error(`Unknown descriptor template: ${template}`);
135135
}
136136

137+
type TapTree = [TapTree, TapTree] | ast.MiniscriptNode;
138+
139+
function getTapLeafScriptNodes(t: ast.DescriptorNode | TapTree): ast.MiniscriptNode[] {
140+
if (Array.isArray(t)) {
141+
if (t.length !== 2) {
142+
throw new Error(`expected tuple, got: ${JSON.stringify(t)}`);
143+
}
144+
return t.map((v) => (Array.isArray(v) ? getTapLeafScriptNodes(v) : v)).flat();
145+
}
146+
147+
if (typeof t === 'object') {
148+
const node = t;
149+
if (!('tr' in node)) {
150+
throw new Error(`TapLeafScripts are only supported for Taproot descriptors, got: ${t}`);
151+
}
152+
if (!Array.isArray(node.tr) || node.tr.length !== 2) {
153+
throw new Error(`expected tuple, got: ${JSON.stringify(node.tr)}`);
154+
}
155+
const tapscript = node.tr[1];
156+
if (!Array.isArray(tapscript)) {
157+
throw new Error(`expected tapscript to be an array, got: ${JSON.stringify(tapscript)}`);
158+
}
159+
return getTapLeafScriptNodes(tapscript);
160+
}
161+
162+
throw new Error(`Invalid input: ${JSON.stringify(t)}`);
163+
}
164+
165+
export function containsKey(script: Miniscript | ast.MiniscriptNode, key: BIP32Interface | string): boolean {
166+
if (script instanceof Miniscript) {
167+
script = ast.fromMiniscript(script);
168+
}
169+
if ('pk' in script) {
170+
return script.pk === toXOnly(key);
171+
}
172+
throw new Error(`Unsupported script type: ${JSON.stringify(script)}`);
173+
}
174+
175+
export function getTapLeafScripts(d: Descriptor): string[] {
176+
return getTapLeafScriptNodes(ast.fromDescriptor(d)).map((n) =>
177+
Miniscript.fromString(ast.formatNode(n), 'tap').toString()
178+
);
179+
}
180+
137181
export function getDescriptor(
138182
template: DescriptorTemplate,
139183
keys: KeyTriple | string[] = getDefaultXPubs(),

modules/utxo-core/src/testutil/descriptor/mock.utils.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Descriptor } from '@bitgo/wasm-miniscript';
1+
import { Descriptor, Miniscript } from '@bitgo/wasm-miniscript';
22
import * as utxolib from '@bitgo/utxo-lib';
33

44
import {
@@ -17,6 +17,7 @@ type BaseMockDescriptorOutputParams = {
1717
index?: number;
1818
value?: bigint;
1919
sequence?: number;
20+
selectTapLeafScript?: Miniscript;
2021
};
2122

2223
function mockOutputId(id?: MockOutputIdParams): {
@@ -42,13 +43,15 @@ export function mockDerivedDescriptorWalletOutput(
4243
value,
4344
},
4445
descriptor,
46+
selectTapLeafScript: outputParams.selectTapLeafScript,
4547
sequence: outputParams.sequence,
4648
};
4749
}
4850

4951
type MockInput = BaseMockDescriptorOutputParams & {
5052
index: number;
5153
descriptor: Descriptor;
54+
selectTapLeafScript?: Miniscript;
5255
};
5356

5457
type MockOutput = {

0 commit comments

Comments
 (0)