Skip to content

Commit

Permalink
Merge pull request #47 from BitGo/BTC-1451.add-updateoutputwithdescri…
Browse files Browse the repository at this point in the history
…ptor

feat: add updateOutputWithDescriptor for WrapPsbt
  • Loading branch information
OttoAllmendinger authored Oct 25, 2024
2 parents 5f5aa92 + 8520223 commit d165f66
Show file tree
Hide file tree
Showing 4 changed files with 217 additions and 35 deletions.
20 changes: 20 additions & 0 deletions packages/wasm-miniscript/src/psbt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,26 @@ impl WrapPsbt {
}
}

#[wasm_bindgen(js_name = updateOutputWithDescriptor)]
pub fn update_output_with_descriptor(
&mut self,
output_index: usize,
descriptor: WrapDescriptor,
) -> Result<(), JsError> {
match descriptor.0 {
WrapDescriptorEnum::Definite(d) => self
.0
.update_output_with_descriptor(output_index, &d)
.map_err(JsError::from),
WrapDescriptorEnum::Derivable(_, _) => Err(JsError::new(
"Cannot update output with a derivable descriptor",
)),
WrapDescriptorEnum::String(_) => Err(JsError::new(
"Cannot update output with a string descriptor",
)),
}
}

#[wasm_bindgen(js_name = finalize)]
pub fn finalize_mut(&mut self) -> Result<(), JsError> {
self.0
Expand Down
82 changes: 49 additions & 33 deletions packages/wasm-miniscript/test/psbt.ts
Original file line number Diff line number Diff line change
@@ -1,59 +1,52 @@
import * as utxolib from "@bitgo/utxo-lib";
import * as assert from "node:assert";
import { getPsbtFixtures, toPsbtWithPrevOutOnly } from "./psbtFixtures";
import { getPsbtFixtures, PsbtStage } from "./psbtFixtures";
import { Descriptor, Psbt } from "../js";

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

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

function toWrappedPsbt(psbt: utxolib.bitgo.UtxoPsbt | Buffer | Uint8Array) {
if (psbt instanceof utxolib.bitgo.UtxoPsbt) {
psbt = psbt.toBuffer();
}
if (psbt instanceof Buffer || psbt instanceof Uint8Array) {
return Psbt.deserialize(psbt);
}
throw new Error("Invalid input");
}

function toUtxoPsbt(psbt: Psbt | Buffer | Uint8Array) {
if (psbt instanceof Psbt) {
psbt = psbt.serialize();
}
if (psbt instanceof Buffer || psbt instanceof Uint8Array) {
return utxolib.bitgo.UtxoPsbt.fromBuffer(Buffer.from(psbt), {
network: utxolib.networks.bitcoin,
});
}
throw new Error("Invalid input");
}

function assertEqualBuffer(a: Buffer | Uint8Array, b: Buffer | Uint8Array, message?: string) {
assert.strictEqual(Buffer.from(a).toString("hex"), Buffer.from(b).toString("hex"), message);
}

const fixtures = getPsbtFixtures(rootWalletKeys);

function getWasmDescriptor(
scriptType: utxolib.bitgo.outputScripts.ScriptType2Of3,
scope: "internal" | "external",
) {
return Descriptor.fromString(
getDescriptorForScriptType(rootWalletKeys, scriptType, scope),
"derivable",
);
}

function describeUpdateInputWithDescriptor(
psbt: utxolib.bitgo.UtxoPsbt,
scriptType: utxolib.bitgo.outputScripts.ScriptType2Of3,
) {
const fullSignedFixture = fixtures.find(
(f) => f.scriptType === scriptType && f.stage === "fullsigned",
);
if (!fullSignedFixture) {
throw new Error("Could not find fullsigned fixture");
function getFixtureAtStage(stage: PsbtStage) {
const f = fixtures.find((f) => f.scriptType === scriptType && f.stage === stage);
if (!f) {
throw new Error(`Could not find fixture for scriptType ${scriptType} and stage ${stage}`);
}
return f;
}

describe("updateInputWithDescriptor", function () {
const descriptorStr = getDescriptorForScriptType(rootWalletKeys, scriptType, "internal");
const index = 0;
const descriptor = Descriptor.fromString(descriptorStr, "derivable");

describe("Wrapped PSBT updateInputWithDescriptor", function () {
it("should update the input with the descriptor", function () {
const descriptorStr = getDescriptorForScriptType(rootWalletKeys, scriptType, "internal");
const index = 0;
const descriptor = Descriptor.fromString(descriptorStr, "derivable");
const wrappedPsbt = toWrappedPsbt(psbt);
wrappedPsbt.updateInputWithDescriptor(0, descriptor.atDerivationIndex(index));
wrappedPsbt.updateOutputWithDescriptor(0, descriptor.atDerivationIndex(index));
const updatedPsbt = toUtxoPsbt(wrappedPsbt);
assertEqualPsbt(updatedPsbt, getFixtureAtStage("unsigned").psbt);
updatedPsbt.signAllInputsHD(rootWalletKeys.triple[0]);
updatedPsbt.signAllInputsHD(rootWalletKeys.triple[2]);
const wrappedSignedPsbt = toWrappedPsbt(updatedPsbt);
Expand All @@ -63,11 +56,34 @@ function describeUpdateInputWithDescriptor(
assertEqualBuffer(updatedPsbt.toBuffer(), wrappedSignedPsbt.serialize());

assertEqualBuffer(
fullSignedFixture.psbt.clone().finalizeAllInputs().extractTransaction().toBuffer(),
getFixtureAtStage("fullsigned")
.psbt.clone()
.finalizeAllInputs()
.extractTransaction()
.toBuffer(),
updatedPsbt.extractTransaction().toBuffer(),
);
});
});

describe("updateInputWithDescriptor util", function () {
it("should update the input with the descriptor", function () {
const cloned = psbt.clone();
updateInputWithDescriptor(cloned, 0, descriptor.atDerivationIndex(index));
cloned.signAllInputsHD(rootWalletKeys.triple[0]);
cloned.signAllInputsHD(rootWalletKeys.triple[2]);
cloned.finalizeAllInputs();

assertEqualBuffer(
getFixtureAtStage("fullsigned")
.psbt.clone()
.finalizeAllInputs()
.extractTransaction()
.toBuffer(),
cloned.extractTransaction().toBuffer(),
);
});
});
}

fixtures.forEach(({ psbt, scriptType, stage }) => {
Expand Down
145 changes: 145 additions & 0 deletions packages/wasm-miniscript/test/psbt.util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import * as assert from "node:assert";
import * as utxolib from "@bitgo/utxo-lib";
import { Descriptor, Psbt } from "../js";

function toAddress(descriptor: Descriptor, network: utxolib.Network) {
utxolib.address.fromOutputScript(Buffer.from(descriptor.scriptPubkey()), network);
}

export function toWrappedPsbt(psbt: utxolib.bitgo.UtxoPsbt | utxolib.Psbt | Buffer | Uint8Array) {
if (psbt instanceof utxolib.bitgo.UtxoPsbt || psbt instanceof utxolib.Psbt) {
psbt = psbt.toBuffer();
}
if (psbt instanceof Buffer || psbt instanceof Uint8Array) {
return Psbt.deserialize(psbt);
}
throw new Error("Invalid input");
}

export function toUtxoPsbt(psbt: Psbt | Buffer | Uint8Array) {
if (psbt instanceof Psbt) {
psbt = psbt.serialize();
}
if (psbt instanceof Buffer || psbt instanceof Uint8Array) {
return utxolib.bitgo.UtxoPsbt.fromBuffer(Buffer.from(psbt), {
network: utxolib.networks.bitcoin,
});
}
throw new Error("Invalid input");
}

export function updateInputWithDescriptor(
psbt: utxolib.Psbt,
inputIndex: number,
descriptor: Descriptor,
) {
const wrappedPsbt = toWrappedPsbt(psbt);
wrappedPsbt.updateInputWithDescriptor(inputIndex, descriptor);
psbt.data.inputs[inputIndex] = toUtxoPsbt(wrappedPsbt).data.inputs[inputIndex];
}

export function updateOutputWithDescriptor(
psbt: utxolib.Psbt,
outputIndex: number,
descriptor: Descriptor,
) {
const wrappedPsbt = toWrappedPsbt(psbt);
wrappedPsbt.updateOutputWithDescriptor(outputIndex, descriptor);
psbt.data.outputs[outputIndex] = toUtxoPsbt(wrappedPsbt).data.outputs[outputIndex];
}

export function finalizePsbt(psbt: utxolib.Psbt) {
const wrappedPsbt = toWrappedPsbt(psbt);
wrappedPsbt.finalize();
const unwrappedPsbt = toUtxoPsbt(wrappedPsbt);
for (let i = 0; i < psbt.data.inputs.length; i++) {
psbt.data.inputs[i] = unwrappedPsbt.data.inputs[i];
}
}

function toEntries(k: string, v: unknown, path: (string | number)[]): [] | [[string, unknown]] {
if (matchPath(path, ["data", "inputs", any, "sighashType"])) {
return [];
}
if (matchPath(path.slice(-1), ["unknownKeyVals"])) {
if (Array.isArray(v) && v.length === 0) {
return [];
}
}
return [[k, toPlainObject(v, path)]];
}

const any = Symbol("any");

function matchPath(path: (string | number)[], pattern: (string | number | symbol)[]) {
if (path.length !== pattern.length) {
return false;
}
for (let i = 0; i < path.length; i++) {
if (pattern[i] !== any && path[i] !== pattern[i]) {
return false;
}
}
return true;
}

function normalizeBip32Derivation(v: unknown) {
if (!Array.isArray(v)) {
throw new Error("Expected bip32Derivation to be an array");
}
return (
[...v] as {
masterFingerprint: Buffer;
path: string;
}[]
)
.map((e) => {
let { path } = e;
if (path.startsWith("m/")) {
path = path.slice(2);
}
return {
...e,
path,
};
})
.sort((a, b) => a.masterFingerprint.toString().localeCompare(b.masterFingerprint.toString()));
}

function toPlainObject(v: unknown, path: (string | number)[]) {
// psbts have fun getters and other types of irregular properties that we mash into shape here
if (v === null || v === undefined) {
return v;
}
if (
matchPath(path, ["data", "inputs", any, "bip32Derivation"]) ||
matchPath(path, ["data", "outputs", any, "bip32Derivation"])
) {
v = normalizeBip32Derivation(v);
}
switch (typeof v) {
case "number":
case "bigint":
case "string":
case "boolean":
return v;
case "object":
if (v instanceof Buffer || v instanceof Uint8Array) {
return v.toString("hex");
}
if (Array.isArray(v)) {
return v.map((v, i) => toPlainObject(v, [...path, i]));
}
return Object.fromEntries(
Object.entries(v)
.flatMap(([k, v]) => toEntries(k, v, [...path, k]))
.sort(([a], [b]) => a.localeCompare(b)),
);
default:
throw new Error(`Unsupported type: ${typeof v}`);
}
}

export function assertEqualPsbt(a: utxolib.Psbt, b: utxolib.Psbt) {
assert.deepStrictEqual(toPlainObject(a, []), toPlainObject(b, []));
}
5 changes: 3 additions & 2 deletions packages/wasm-miniscript/test/psbtFixtures.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as utxolib from "@bitgo/utxo-lib";
import { RootWalletKeys } from "@bitgo/utxo-lib/dist/src/bitgo";

type PsbtStage = "bare" | "unsigned" | "halfsigned" | "fullsigned";
export type PsbtStage = "bare" | "unsigned" | "halfsigned" | "fullsigned";

export function toPsbtWithPrevOutOnly(psbt: utxolib.bitgo.UtxoPsbt) {
const psbtCopy = utxolib.bitgo.UtxoPsbt.createPsbt({
Expand Down Expand Up @@ -43,7 +43,8 @@ function getPsbtWithScriptTypeAndStage(
[
{
value: BigInt(1e8 - 1000),
scriptType: "p2sh",
scriptType,
isInternalAddress: true,
},
],
utxolib.networks.bitcoin,
Expand Down

0 comments on commit d165f66

Please sign in to comment.