Skip to content

Commit d165f66

Browse files
Merge pull request #47 from BitGo/BTC-1451.add-updateoutputwithdescriptor
feat: add updateOutputWithDescriptor for WrapPsbt
2 parents 5f5aa92 + 8520223 commit d165f66

File tree

4 files changed

+217
-35
lines changed

4 files changed

+217
-35
lines changed

packages/wasm-miniscript/src/psbt.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,26 @@ impl WrapPsbt {
4343
}
4444
}
4545

46+
#[wasm_bindgen(js_name = updateOutputWithDescriptor)]
47+
pub fn update_output_with_descriptor(
48+
&mut self,
49+
output_index: usize,
50+
descriptor: WrapDescriptor,
51+
) -> Result<(), JsError> {
52+
match descriptor.0 {
53+
WrapDescriptorEnum::Definite(d) => self
54+
.0
55+
.update_output_with_descriptor(output_index, &d)
56+
.map_err(JsError::from),
57+
WrapDescriptorEnum::Derivable(_, _) => Err(JsError::new(
58+
"Cannot update output with a derivable descriptor",
59+
)),
60+
WrapDescriptorEnum::String(_) => Err(JsError::new(
61+
"Cannot update output with a string descriptor",
62+
)),
63+
}
64+
}
65+
4666
#[wasm_bindgen(js_name = finalize)]
4767
pub fn finalize_mut(&mut self) -> Result<(), JsError> {
4868
self.0

packages/wasm-miniscript/test/psbt.ts

Lines changed: 49 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,52 @@
11
import * as utxolib from "@bitgo/utxo-lib";
22
import * as assert from "node:assert";
3-
import { getPsbtFixtures, toPsbtWithPrevOutOnly } from "./psbtFixtures";
3+
import { getPsbtFixtures, PsbtStage } from "./psbtFixtures";
44
import { Descriptor, Psbt } from "../js";
55

66
import { getDescriptorForScriptType } from "./descriptorUtil";
7+
import { assertEqualPsbt, toUtxoPsbt, toWrappedPsbt, updateInputWithDescriptor } from "./psbt.util";
78

89
const rootWalletKeys = new utxolib.bitgo.RootWalletKeys(utxolib.testutil.getKeyTriple("wasm"));
910

10-
function toWrappedPsbt(psbt: utxolib.bitgo.UtxoPsbt | Buffer | Uint8Array) {
11-
if (psbt instanceof utxolib.bitgo.UtxoPsbt) {
12-
psbt = psbt.toBuffer();
13-
}
14-
if (psbt instanceof Buffer || psbt instanceof Uint8Array) {
15-
return Psbt.deserialize(psbt);
16-
}
17-
throw new Error("Invalid input");
18-
}
19-
20-
function toUtxoPsbt(psbt: Psbt | Buffer | Uint8Array) {
21-
if (psbt instanceof Psbt) {
22-
psbt = psbt.serialize();
23-
}
24-
if (psbt instanceof Buffer || psbt instanceof Uint8Array) {
25-
return utxolib.bitgo.UtxoPsbt.fromBuffer(Buffer.from(psbt), {
26-
network: utxolib.networks.bitcoin,
27-
});
28-
}
29-
throw new Error("Invalid input");
30-
}
31-
3211
function assertEqualBuffer(a: Buffer | Uint8Array, b: Buffer | Uint8Array, message?: string) {
3312
assert.strictEqual(Buffer.from(a).toString("hex"), Buffer.from(b).toString("hex"), message);
3413
}
3514

3615
const fixtures = getPsbtFixtures(rootWalletKeys);
3716

17+
function getWasmDescriptor(
18+
scriptType: utxolib.bitgo.outputScripts.ScriptType2Of3,
19+
scope: "internal" | "external",
20+
) {
21+
return Descriptor.fromString(
22+
getDescriptorForScriptType(rootWalletKeys, scriptType, scope),
23+
"derivable",
24+
);
25+
}
26+
3827
function describeUpdateInputWithDescriptor(
3928
psbt: utxolib.bitgo.UtxoPsbt,
4029
scriptType: utxolib.bitgo.outputScripts.ScriptType2Of3,
4130
) {
42-
const fullSignedFixture = fixtures.find(
43-
(f) => f.scriptType === scriptType && f.stage === "fullsigned",
44-
);
45-
if (!fullSignedFixture) {
46-
throw new Error("Could not find fullsigned fixture");
31+
function getFixtureAtStage(stage: PsbtStage) {
32+
const f = fixtures.find((f) => f.scriptType === scriptType && f.stage === stage);
33+
if (!f) {
34+
throw new Error(`Could not find fixture for scriptType ${scriptType} and stage ${stage}`);
35+
}
36+
return f;
4737
}
4838

49-
describe("updateInputWithDescriptor", function () {
39+
const descriptorStr = getDescriptorForScriptType(rootWalletKeys, scriptType, "internal");
40+
const index = 0;
41+
const descriptor = Descriptor.fromString(descriptorStr, "derivable");
42+
43+
describe("Wrapped PSBT updateInputWithDescriptor", function () {
5044
it("should update the input with the descriptor", function () {
51-
const descriptorStr = getDescriptorForScriptType(rootWalletKeys, scriptType, "internal");
52-
const index = 0;
53-
const descriptor = Descriptor.fromString(descriptorStr, "derivable");
5445
const wrappedPsbt = toWrappedPsbt(psbt);
5546
wrappedPsbt.updateInputWithDescriptor(0, descriptor.atDerivationIndex(index));
47+
wrappedPsbt.updateOutputWithDescriptor(0, descriptor.atDerivationIndex(index));
5648
const updatedPsbt = toUtxoPsbt(wrappedPsbt);
49+
assertEqualPsbt(updatedPsbt, getFixtureAtStage("unsigned").psbt);
5750
updatedPsbt.signAllInputsHD(rootWalletKeys.triple[0]);
5851
updatedPsbt.signAllInputsHD(rootWalletKeys.triple[2]);
5952
const wrappedSignedPsbt = toWrappedPsbt(updatedPsbt);
@@ -63,11 +56,34 @@ function describeUpdateInputWithDescriptor(
6356
assertEqualBuffer(updatedPsbt.toBuffer(), wrappedSignedPsbt.serialize());
6457

6558
assertEqualBuffer(
66-
fullSignedFixture.psbt.clone().finalizeAllInputs().extractTransaction().toBuffer(),
59+
getFixtureAtStage("fullsigned")
60+
.psbt.clone()
61+
.finalizeAllInputs()
62+
.extractTransaction()
63+
.toBuffer(),
6764
updatedPsbt.extractTransaction().toBuffer(),
6865
);
6966
});
7067
});
68+
69+
describe("updateInputWithDescriptor util", function () {
70+
it("should update the input with the descriptor", function () {
71+
const cloned = psbt.clone();
72+
updateInputWithDescriptor(cloned, 0, descriptor.atDerivationIndex(index));
73+
cloned.signAllInputsHD(rootWalletKeys.triple[0]);
74+
cloned.signAllInputsHD(rootWalletKeys.triple[2]);
75+
cloned.finalizeAllInputs();
76+
77+
assertEqualBuffer(
78+
getFixtureAtStage("fullsigned")
79+
.psbt.clone()
80+
.finalizeAllInputs()
81+
.extractTransaction()
82+
.toBuffer(),
83+
cloned.extractTransaction().toBuffer(),
84+
);
85+
});
86+
});
7187
}
7288

7389
fixtures.forEach(({ psbt, scriptType, stage }) => {
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import * as assert from "node:assert";
2+
import * as utxolib from "@bitgo/utxo-lib";
3+
import { Descriptor, Psbt } from "../js";
4+
5+
function toAddress(descriptor: Descriptor, network: utxolib.Network) {
6+
utxolib.address.fromOutputScript(Buffer.from(descriptor.scriptPubkey()), network);
7+
}
8+
9+
export function toWrappedPsbt(psbt: utxolib.bitgo.UtxoPsbt | utxolib.Psbt | Buffer | Uint8Array) {
10+
if (psbt instanceof utxolib.bitgo.UtxoPsbt || psbt instanceof utxolib.Psbt) {
11+
psbt = psbt.toBuffer();
12+
}
13+
if (psbt instanceof Buffer || psbt instanceof Uint8Array) {
14+
return Psbt.deserialize(psbt);
15+
}
16+
throw new Error("Invalid input");
17+
}
18+
19+
export function toUtxoPsbt(psbt: Psbt | Buffer | Uint8Array) {
20+
if (psbt instanceof Psbt) {
21+
psbt = psbt.serialize();
22+
}
23+
if (psbt instanceof Buffer || psbt instanceof Uint8Array) {
24+
return utxolib.bitgo.UtxoPsbt.fromBuffer(Buffer.from(psbt), {
25+
network: utxolib.networks.bitcoin,
26+
});
27+
}
28+
throw new Error("Invalid input");
29+
}
30+
31+
export function updateInputWithDescriptor(
32+
psbt: utxolib.Psbt,
33+
inputIndex: number,
34+
descriptor: Descriptor,
35+
) {
36+
const wrappedPsbt = toWrappedPsbt(psbt);
37+
wrappedPsbt.updateInputWithDescriptor(inputIndex, descriptor);
38+
psbt.data.inputs[inputIndex] = toUtxoPsbt(wrappedPsbt).data.inputs[inputIndex];
39+
}
40+
41+
export function updateOutputWithDescriptor(
42+
psbt: utxolib.Psbt,
43+
outputIndex: number,
44+
descriptor: Descriptor,
45+
) {
46+
const wrappedPsbt = toWrappedPsbt(psbt);
47+
wrappedPsbt.updateOutputWithDescriptor(outputIndex, descriptor);
48+
psbt.data.outputs[outputIndex] = toUtxoPsbt(wrappedPsbt).data.outputs[outputIndex];
49+
}
50+
51+
export function finalizePsbt(psbt: utxolib.Psbt) {
52+
const wrappedPsbt = toWrappedPsbt(psbt);
53+
wrappedPsbt.finalize();
54+
const unwrappedPsbt = toUtxoPsbt(wrappedPsbt);
55+
for (let i = 0; i < psbt.data.inputs.length; i++) {
56+
psbt.data.inputs[i] = unwrappedPsbt.data.inputs[i];
57+
}
58+
}
59+
60+
function toEntries(k: string, v: unknown, path: (string | number)[]): [] | [[string, unknown]] {
61+
if (matchPath(path, ["data", "inputs", any, "sighashType"])) {
62+
return [];
63+
}
64+
if (matchPath(path.slice(-1), ["unknownKeyVals"])) {
65+
if (Array.isArray(v) && v.length === 0) {
66+
return [];
67+
}
68+
}
69+
return [[k, toPlainObject(v, path)]];
70+
}
71+
72+
const any = Symbol("any");
73+
74+
function matchPath(path: (string | number)[], pattern: (string | number | symbol)[]) {
75+
if (path.length !== pattern.length) {
76+
return false;
77+
}
78+
for (let i = 0; i < path.length; i++) {
79+
if (pattern[i] !== any && path[i] !== pattern[i]) {
80+
return false;
81+
}
82+
}
83+
return true;
84+
}
85+
86+
function normalizeBip32Derivation(v: unknown) {
87+
if (!Array.isArray(v)) {
88+
throw new Error("Expected bip32Derivation to be an array");
89+
}
90+
return (
91+
[...v] as {
92+
masterFingerprint: Buffer;
93+
path: string;
94+
}[]
95+
)
96+
.map((e) => {
97+
let { path } = e;
98+
if (path.startsWith("m/")) {
99+
path = path.slice(2);
100+
}
101+
return {
102+
...e,
103+
path,
104+
};
105+
})
106+
.sort((a, b) => a.masterFingerprint.toString().localeCompare(b.masterFingerprint.toString()));
107+
}
108+
109+
function toPlainObject(v: unknown, path: (string | number)[]) {
110+
// psbts have fun getters and other types of irregular properties that we mash into shape here
111+
if (v === null || v === undefined) {
112+
return v;
113+
}
114+
if (
115+
matchPath(path, ["data", "inputs", any, "bip32Derivation"]) ||
116+
matchPath(path, ["data", "outputs", any, "bip32Derivation"])
117+
) {
118+
v = normalizeBip32Derivation(v);
119+
}
120+
switch (typeof v) {
121+
case "number":
122+
case "bigint":
123+
case "string":
124+
case "boolean":
125+
return v;
126+
case "object":
127+
if (v instanceof Buffer || v instanceof Uint8Array) {
128+
return v.toString("hex");
129+
}
130+
if (Array.isArray(v)) {
131+
return v.map((v, i) => toPlainObject(v, [...path, i]));
132+
}
133+
return Object.fromEntries(
134+
Object.entries(v)
135+
.flatMap(([k, v]) => toEntries(k, v, [...path, k]))
136+
.sort(([a], [b]) => a.localeCompare(b)),
137+
);
138+
default:
139+
throw new Error(`Unsupported type: ${typeof v}`);
140+
}
141+
}
142+
143+
export function assertEqualPsbt(a: utxolib.Psbt, b: utxolib.Psbt) {
144+
assert.deepStrictEqual(toPlainObject(a, []), toPlainObject(b, []));
145+
}

packages/wasm-miniscript/test/psbtFixtures.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as utxolib from "@bitgo/utxo-lib";
22
import { RootWalletKeys } from "@bitgo/utxo-lib/dist/src/bitgo";
33

4-
type PsbtStage = "bare" | "unsigned" | "halfsigned" | "fullsigned";
4+
export type PsbtStage = "bare" | "unsigned" | "halfsigned" | "fullsigned";
55

66
export function toPsbtWithPrevOutOnly(psbt: utxolib.bitgo.UtxoPsbt) {
77
const psbtCopy = utxolib.bitgo.UtxoPsbt.createPsbt({
@@ -43,7 +43,8 @@ function getPsbtWithScriptTypeAndStage(
4343
[
4444
{
4545
value: BigInt(1e8 - 1000),
46-
scriptType: "p2sh",
46+
scriptType,
47+
isInternalAddress: true,
4748
},
4849
],
4950
utxolib.networks.bitcoin,

0 commit comments

Comments
 (0)