Skip to content

Commit ae9823b

Browse files
feat(psbt): add signWithXprv method
Add PSBT signing functionality using private extended keys (xprv). Supports individual key signing and returns a map of signed pubkeys. Issue: BTC-1845
1 parent 233220b commit ae9823b

File tree

3 files changed

+91
-15
lines changed

3 files changed

+91
-15
lines changed

packages/wasm-miniscript/src/psbt.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1+
use std::str::FromStr;
12
use crate::descriptor::WrapDescriptorEnum;
23
use crate::WrapDescriptor;
34
use miniscript::bitcoin::secp256k1::Secp256k1;
45
use miniscript::bitcoin::Psbt;
56
use miniscript::psbt::PsbtExt;
67
use wasm_bindgen::prelude::wasm_bindgen;
7-
use wasm_bindgen::JsError;
8+
use wasm_bindgen::{JsError, JsValue};
9+
use crate::try_into_js_value::TryIntoJsValue;
810

911
#[wasm_bindgen]
1012
pub struct WrapPsbt(Psbt);
@@ -63,6 +65,16 @@ impl WrapPsbt {
6365
}
6466
}
6567

68+
#[wasm_bindgen(js_name = signWithXprv)]
69+
pub fn sign_with_xprv(&mut self, xprv: String) -> Result<JsValue, JsError> {
70+
let key = miniscript::bitcoin::bip32::Xpriv::from_str(&xprv)
71+
.map_err(|_| JsError::new("Invalid xprv"))?;
72+
self.0
73+
.sign(&key, &Secp256k1::new())
74+
.map_err(|(_, errors)| JsError::new(&format!("{} errors: {:?}", errors.len(), errors)))
75+
.and_then(|r| r.try_to_js_value())
76+
}
77+
6678
#[wasm_bindgen(js_name = finalize)]
6779
pub fn finalize_mut(&mut self) -> Result<(), JsError> {
6880
self.0

packages/wasm-miniscript/src/try_into_js_value.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use miniscript::{
77
MiniscriptKey, RelLockTime, ScriptContext, Terminal, Threshold,
88
};
99
use std::sync::Arc;
10+
use miniscript::bitcoin::psbt::{SigningKeys, SigningKeysMap};
1011
use wasm_bindgen::{JsError, JsValue};
1112

1213
pub(crate) trait TryIntoJsValue {
@@ -256,3 +257,28 @@ impl TryIntoJsValue for DescriptorType {
256257
Ok(JsValue::from_str(&str_from_enum))
257258
}
258259
}
260+
261+
262+
impl TryIntoJsValue for SigningKeys {
263+
fn try_to_js_value(&self) -> Result<JsValue, JsError> {
264+
match self {
265+
SigningKeys::Ecdsa(v) => {
266+
js_obj!("Ecdsa" => v)
267+
}
268+
SigningKeys::Schnorr(v) => {
269+
js_obj!("Schnorr" => v)
270+
}
271+
}
272+
}
273+
}
274+
275+
impl TryIntoJsValue for SigningKeysMap {
276+
fn try_to_js_value(&self) -> Result<JsValue, JsError> {
277+
let obj = js_sys::Object::new();
278+
for (key, value) in self.iter() {
279+
js_sys::Reflect::set(&obj, &key.to_string().into(), &value.try_to_js_value()?.into())
280+
.map_err(|_| JsError::new("Failed to set object property"))?;
281+
}
282+
Ok(obj.into())
283+
}
284+
}

packages/wasm-miniscript/test/psbt.ts

Lines changed: 52 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { Descriptor, Psbt } from "../js";
55

66
import { getDescriptorForScriptType } from "./descriptorUtil";
77
import { assertEqualPsbt, toUtxoPsbt, toWrappedPsbt, updateInputWithDescriptor } from "./psbt.util";
8+
import { getKey } from "@bitgo/utxo-lib/dist/src/testutil";
89

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

@@ -14,16 +15,6 @@ function assertEqualBuffer(a: Buffer | Uint8Array, b: Buffer | Uint8Array, messa
1415

1516
const fixtures = getPsbtFixtures(rootWalletKeys);
1617

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-
2718
function describeUpdateInputWithDescriptor(
2819
psbt: utxolib.bitgo.UtxoPsbt,
2920
scriptType: utxolib.bitgo.outputScripts.ScriptType2Of3,
@@ -40,12 +31,21 @@ function describeUpdateInputWithDescriptor(
4031
const index = 0;
4132
const descriptor = Descriptor.fromString(descriptorStr, "derivable");
4233

34+
function getWrappedPsbt() {
35+
return toWrappedPsbt(psbt);
36+
}
37+
38+
function getWrappedPsbtWithDescriptorInfo(): Psbt {
39+
const wrappedPsbt = getWrappedPsbt();
40+
const descriptorAtDerivation = descriptor.atDerivationIndex(index);
41+
wrappedPsbt.updateInputWithDescriptor(0, descriptorAtDerivation);
42+
wrappedPsbt.updateOutputWithDescriptor(0, descriptorAtDerivation);
43+
return wrappedPsbt;
44+
}
45+
4346
describe("Wrapped PSBT updateInputWithDescriptor", function () {
4447
it("should update the input with the descriptor", function () {
45-
const wrappedPsbt = toWrappedPsbt(psbt);
46-
const descriptorAtDerivation = descriptor.atDerivationIndex(index);
47-
wrappedPsbt.updateInputWithDescriptor(0, descriptorAtDerivation);
48-
wrappedPsbt.updateOutputWithDescriptor(0, descriptorAtDerivation);
48+
const wrappedPsbt = getWrappedPsbtWithDescriptorInfo();
4949
const updatedPsbt = toUtxoPsbt(wrappedPsbt);
5050
assertEqualPsbt(updatedPsbt, getFixtureAtStage("unsigned").psbt);
5151
updatedPsbt.signAllInputsHD(rootWalletKeys.triple[0]);
@@ -85,6 +85,44 @@ function describeUpdateInputWithDescriptor(
8585
);
8686
});
8787
});
88+
89+
describe("psbt signWithXprv", function () {
90+
type KeyName = utxolib.bitgo.KeyName | "unrelated";
91+
function signWithKey(keys: KeyName[], { checkFinalized = false } = {}) {
92+
it(`signs the input with keys ${keys}`, function () {
93+
const psbt = getWrappedPsbtWithDescriptorInfo();
94+
keys.forEach((keyName) => {
95+
const key = keyName === "unrelated" ? getKey(keyName) : rootWalletKeys[keyName];
96+
const derivationPaths = toUtxoPsbt(psbt).data.inputs[0].bip32Derivation.map(
97+
(d) => d.path,
98+
);
99+
assert.ok(derivationPaths.every((p) => p === derivationPaths[0]));
100+
const derived = key.derivePath(derivationPaths[0]);
101+
assert.deepStrictEqual(psbt.signWithXprv(key.toBase58()), {
102+
// map: input index -> pubkey array
103+
0: { Ecdsa: keyName === "unrelated" ? [] : [derived.publicKey.toString("hex")] },
104+
});
105+
});
106+
107+
if (checkFinalized) {
108+
psbt.finalize();
109+
assertEqualBuffer(
110+
toUtxoPsbt(psbt).extractTransaction().toBuffer(),
111+
getFixtureAtStage("fullsigned")
112+
.psbt.finalizeAllInputs()
113+
.extractTransaction()
114+
.toBuffer(),
115+
);
116+
}
117+
});
118+
}
119+
120+
signWithKey(["user"]);
121+
signWithKey(["backup"]);
122+
signWithKey(["bitgo"]);
123+
signWithKey(["unrelated"]);
124+
signWithKey(["user", "bitgo"], { checkFinalized: true });
125+
});
88126
}
89127

90128
fixtures.forEach(({ psbt, scriptType, stage }) => {

0 commit comments

Comments
 (0)