Skip to content

Commit 8ddb49c

Browse files
Merge pull request #38 from BitGo/BTC-1348.psbt.add-updateinput
feat: wrap updateInputWithDescriptor
2 parents 7153476 + 4e71de8 commit 8ddb49c

File tree

6 files changed

+172
-73
lines changed

6 files changed

+172
-73
lines changed

packages/wasm-miniscript/src/descriptor.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,14 @@ use std::str::FromStr;
77
use wasm_bindgen::prelude::wasm_bindgen;
88
use wasm_bindgen::{JsError, JsValue};
99

10-
enum WrapDescriptorEnum {
10+
pub(crate) enum WrapDescriptorEnum {
1111
Derivable(Descriptor<DescriptorPublicKey>, KeyMap),
1212
Definite(Descriptor<DefiniteDescriptorKey>),
1313
String(Descriptor<String>),
1414
}
1515

1616
#[wasm_bindgen]
17-
pub struct WrapDescriptor(WrapDescriptorEnum);
17+
pub struct WrapDescriptor(pub(crate) WrapDescriptorEnum);
1818

1919
#[wasm_bindgen]
2020
impl WrapDescriptor {

packages/wasm-miniscript/src/psbt.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
use miniscript::bitcoin::Psbt;
2+
use miniscript::psbt::PsbtExt;
23
use wasm_bindgen::prelude::wasm_bindgen;
34
use wasm_bindgen::{JsError};
5+
use crate::descriptor::WrapDescriptorEnum;
6+
use crate::WrapDescriptor;
47

58
#[wasm_bindgen]
69
pub struct WrapPsbt(Psbt);
@@ -14,4 +17,19 @@ impl WrapPsbt {
1417
pub fn serialize(&self) -> Vec<u8> {
1518
self.0.serialize()
1619
}
20+
21+
#[wasm_bindgen(js_name = updateInputWithDescriptor)]
22+
pub fn update_input_with_descriptor(&mut self, input_index: usize, descriptor: WrapDescriptor) -> Result<(), JsError> {
23+
match descriptor.0 {
24+
WrapDescriptorEnum::Definite(d) => {
25+
self.0.update_input_with_descriptor(input_index, &d).map_err(JsError::from)
26+
}
27+
WrapDescriptorEnum::Derivable(_, _) => {
28+
Err(JsError::new("Cannot update input with a derivable descriptor"))
29+
}
30+
WrapDescriptorEnum::String(_) => {
31+
Err(JsError::new("Cannot update input with a string descriptor"))
32+
}
33+
}
34+
}
1735
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import * as utxolib from "@bitgo/utxo-lib";
2+
3+
/** Expand a template with the given root wallet keys and chain code */
4+
function expand(template: string, rootWalletKeys: utxolib.bitgo.RootWalletKeys, chainCode: number) {
5+
return template.replace(/\$([0-9])/g, (_, i) => {
6+
const keyIndex = parseInt(i, 10);
7+
if (keyIndex !== 0 && keyIndex !== 1 && keyIndex !== 2) {
8+
throw new Error("Invalid key index");
9+
}
10+
const xpub = rootWalletKeys.triple[keyIndex].neutered().toBase58();
11+
const prefix = rootWalletKeys.derivationPrefixes[keyIndex];
12+
return xpub + "/" + prefix + "/" + chainCode + "/*";
13+
});
14+
}
15+
16+
/**
17+
* Get a standard output descriptor that corresponds to the proprietary HD wallet setup
18+
* used in BitGo wallets.
19+
* Only supports a subset of script types.
20+
*/
21+
export function getDescriptorForScriptType(
22+
rootWalletKeys: utxolib.bitgo.RootWalletKeys,
23+
scriptType: utxolib.bitgo.outputScripts.ScriptType2Of3,
24+
scope: "internal" | "external",
25+
): string {
26+
const chain =
27+
scope === "external"
28+
? utxolib.bitgo.getExternalChainCode(scriptType)
29+
: utxolib.bitgo.getInternalChainCode(scriptType);
30+
switch (scriptType) {
31+
case "p2sh":
32+
return expand("sh(multi(2,$0,$1,$2))", rootWalletKeys, chain);
33+
case "p2shP2wsh":
34+
return expand("sh(wsh(multi(2,$0,$1,$2)))", rootWalletKeys, chain);
35+
case "p2wsh":
36+
return expand("wsh(multi(2,$0,$1,$2))", rootWalletKeys, chain);
37+
default:
38+
throw new Error(`Unsupported script type ${scriptType}`);
39+
}
40+
}

packages/wasm-miniscript/test/fixedScriptToDescriptor.ts

Lines changed: 1 addition & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,7 @@
11
import * as assert from "assert";
22
import * as utxolib from "@bitgo/utxo-lib";
33
import { Descriptor } from "../js";
4-
5-
/** Expand a template with the given root wallet keys and chain code */
6-
function expand(template: string, rootWalletKeys: utxolib.bitgo.RootWalletKeys, chainCode: number) {
7-
return template.replace(/\$([0-9])/g, (_, i) => {
8-
const keyIndex = parseInt(i, 10);
9-
if (keyIndex !== 0 && keyIndex !== 1 && keyIndex !== 2) {
10-
throw new Error("Invalid key index");
11-
}
12-
const xpub = rootWalletKeys.triple[keyIndex].neutered().toBase58();
13-
const prefix = rootWalletKeys.derivationPrefixes[keyIndex];
14-
return xpub + "/" + prefix + "/" + chainCode + "/*";
15-
});
16-
}
17-
18-
/**
19-
* Get a standard output descriptor that corresponds to the proprietary HD wallet setup
20-
* used in BitGo wallets.
21-
* Only supports a subset of script types.
22-
*/
23-
function getDescriptorForScriptType(
24-
rootWalletKeys: utxolib.bitgo.RootWalletKeys,
25-
scriptType: utxolib.bitgo.outputScripts.ScriptType2Of3,
26-
scope: "internal" | "external",
27-
): string {
28-
const chain =
29-
scope === "external"
30-
? utxolib.bitgo.getExternalChainCode(scriptType)
31-
: utxolib.bitgo.getInternalChainCode(scriptType);
32-
switch (scriptType) {
33-
case "p2sh":
34-
return expand("sh(multi(2,$0,$1,$2))", rootWalletKeys, chain);
35-
case "p2shP2wsh":
36-
return expand("sh(wsh(multi(2,$0,$1,$2)))", rootWalletKeys, chain);
37-
case "p2wsh":
38-
return expand("wsh(multi(2,$0,$1,$2))", rootWalletKeys, chain);
39-
default:
40-
throw new Error(`Unsupported script type ${scriptType}`);
41-
}
42-
}
4+
import { getDescriptorForScriptType } from "./descriptorUtil";
435

446
const rootWalletKeys = new utxolib.bitgo.RootWalletKeys(utxolib.testutil.getKeyTriple("wasm"));
457
const scriptTypes = ["p2sh", "p2shP2wsh", "p2wsh"] as const;
Lines changed: 74 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,91 @@
11
import * as utxolib from "@bitgo/utxo-lib";
22
import * as assert from "node:assert";
3-
import { getPsbtFixtures } from "./psbtFixtures";
4-
import { Psbt } from "../js";
3+
import { getPsbtFixtures, toPsbtWithPrevOutOnly } from "./psbtFixtures";
4+
import { Descriptor, Psbt } from "../js";
55

6-
getPsbtFixtures().forEach(({ psbt, name }) => {
7-
describe(`PSBT fixture ${name}`, function () {
6+
import { getDescriptorForScriptType } from "./descriptorUtil";
7+
8+
const rootWalletKeys = new utxolib.bitgo.RootWalletKeys(utxolib.testutil.getKeyTriple("wasm"));
9+
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+
32+
const fixtures = getPsbtFixtures(rootWalletKeys);
33+
34+
function describeUpdateInputWithDescriptor(
35+
psbt: utxolib.bitgo.UtxoPsbt,
36+
scriptType: utxolib.bitgo.outputScripts.ScriptType2Of3,
37+
) {
38+
const fullSignedFixture = fixtures.find(
39+
(f) => f.scriptType === scriptType && f.stage === "fullsigned",
40+
);
41+
if (!fullSignedFixture) {
42+
throw new Error("Could not find fullsigned fixture");
43+
}
44+
45+
describe("updateInputWithDescriptor", function () {
46+
it("should update the input with the descriptor", function () {
47+
const descriptorStr = getDescriptorForScriptType(rootWalletKeys, scriptType, "internal");
48+
const index = 0;
49+
const descriptor = Descriptor.fromString(descriptorStr, "derivable");
50+
const wrappedPsbt = toWrappedPsbt(toPsbtWithPrevOutOnly(psbt));
51+
wrappedPsbt.updateInputWithDescriptor(0, descriptor.atDerivationIndex(index));
52+
const updatedPsbt = toUtxoPsbt(wrappedPsbt);
53+
updatedPsbt.signAllInputsHD(rootWalletKeys.triple[0]);
54+
updatedPsbt.signAllInputsHD(rootWalletKeys.triple[2]);
55+
updatedPsbt.finalizeAllInputs();
56+
assert.deepStrictEqual(
57+
fullSignedFixture.psbt
58+
.clone()
59+
.finalizeAllInputs()
60+
.extractTransaction()
61+
.toBuffer()
62+
.toString("hex"),
63+
updatedPsbt.extractTransaction().toBuffer().toString("hex"),
64+
);
65+
});
66+
});
67+
}
68+
69+
fixtures.forEach(({ psbt, scriptType, stage }) => {
70+
describe(`PSBT fixture ${scriptType} ${stage}`, function () {
871
let buf: Buffer;
972
let wrappedPsbt: Psbt;
1073

1174
before(function () {
1275
buf = psbt.toBuffer();
13-
wrappedPsbt = Psbt.deserialize(buf);
76+
wrappedPsbt = toWrappedPsbt(buf);
1477
});
1578

1679
it("should map to same hex", function () {
17-
assert.strictEqual(
18-
buf.toString("hex"),
19-
// it seems that the utxolib impl sometimes adds two extra bytes zero bytes at the end
20-
// they probably are insignificant so we just add them here
21-
Buffer.from(wrappedPsbt.serialize()).toString("hex") + (name === "empty" ? "0000" : ""),
22-
);
80+
assert.strictEqual(buf.toString("hex"), Buffer.from(wrappedPsbt.serialize()).toString("hex"));
2381
});
2482

2583
it("should round-trip utxolib -> ms -> utxolib", function () {
26-
assert.strictEqual(
27-
buf.toString("hex"),
28-
utxolib.bitgo.UtxoPsbt.fromBuffer(Buffer.from(wrappedPsbt.serialize()), {
29-
network: utxolib.networks.bitcoin,
30-
})
31-
.toBuffer()
32-
.toString("hex"),
33-
);
84+
assert.strictEqual(buf.toString("hex"), toUtxoPsbt(wrappedPsbt).toBuffer().toString("hex"));
3485
});
86+
87+
if (stage === "bare") {
88+
describeUpdateInputWithDescriptor(psbt, scriptType);
89+
}
3590
});
3691
});
Lines changed: 37 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,38 @@
11
import * as utxolib from "@bitgo/utxo-lib";
2+
import { RootWalletKeys } from "@bitgo/utxo-lib/dist/src/bitgo";
23

3-
function getEmptyPsbt() {
4-
return new utxolib.bitgo.UtxoPsbt();
4+
type PsbtStage = "bare" | "unsigned" | "halfsigned" | "fullsigned";
5+
6+
export function toPsbtWithPrevOutOnly(psbt: utxolib.bitgo.UtxoPsbt) {
7+
const psbtCopy = utxolib.bitgo.UtxoPsbt.createPsbt({
8+
network: utxolib.networks.bitcoin,
9+
});
10+
psbtCopy.setVersion(psbt.version);
11+
psbtCopy.setLocktime(psbt.locktime);
12+
psbt.txInputs.forEach((input, vin) => {
13+
const { witnessUtxo, nonWitnessUtxo } = psbt.data.inputs[vin];
14+
psbtCopy.addInput({
15+
hash: input.hash,
16+
index: input.index,
17+
sequence: input.sequence,
18+
...(witnessUtxo ? { witnessUtxo } : { nonWitnessUtxo }),
19+
});
20+
});
21+
psbt.txOutputs.forEach((output, vout) => {
22+
psbtCopy.addOutput(output);
23+
});
24+
return psbtCopy;
525
}
626

727
function getPsbtWithScriptTypeAndStage(
8-
seed: string,
28+
keys: RootWalletKeys,
929
scriptType: utxolib.bitgo.outputScripts.ScriptType2Of3,
10-
stage: "unsigned" | "halfsigned" | "fullsigned",
30+
stage: PsbtStage,
1131
) {
12-
const keys = new utxolib.bitgo.RootWalletKeys(utxolib.testutil.getKeyTriple(seed));
32+
if (stage === "bare") {
33+
const psbt = getPsbtWithScriptTypeAndStage(keys, scriptType, "unsigned");
34+
return toPsbtWithPrevOutOnly(psbt);
35+
}
1336
return utxolib.testutil.constructPsbt(
1437
[
1538
{
@@ -25,27 +48,28 @@ function getPsbtWithScriptTypeAndStage(
2548
],
2649
utxolib.networks.bitcoin,
2750
keys,
28-
"unsigned",
51+
stage,
2952
);
3053
}
3154

3255
export type PsbtFixture = {
3356
psbt: utxolib.bitgo.UtxoPsbt;
34-
name: string;
57+
scriptType: utxolib.bitgo.outputScripts.ScriptType2Of3;
58+
stage: PsbtStage;
3559
};
3660

37-
export function getPsbtFixtures(): PsbtFixture[] {
61+
export function getPsbtFixtures(keys: RootWalletKeys): PsbtFixture[] {
3862
const testMatrixScriptTypes = ["p2sh", "p2shP2wsh", "p2wsh"] as const;
39-
const testMatrixStages = ["unsigned", "halfsigned", "fullsigned"] as const;
63+
const testMatrixStages = ["bare", "unsigned", "halfsigned", "fullsigned"] as const;
4064

41-
const fixturesBitGo2Of3 = testMatrixStages.flatMap((stage) => {
65+
return testMatrixStages.flatMap((stage) => {
4266
return testMatrixScriptTypes.map((scriptType) => {
4367
return {
44-
psbt: getPsbtWithScriptTypeAndStage("wasm", scriptType, stage),
68+
psbt: getPsbtWithScriptTypeAndStage(keys, scriptType, stage),
4569
name: `${scriptType}-${stage}`,
70+
scriptType,
71+
stage,
4672
};
4773
});
4874
});
49-
50-
return [{ psbt: getEmptyPsbt(), name: "empty" }, ...fixturesBitGo2Of3];
5175
}

0 commit comments

Comments
 (0)