Skip to content

Commit ae600dd

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 5b7e493 commit ae600dd

File tree

4 files changed

+102
-15
lines changed

4 files changed

+102
-15
lines changed

packages/wasm-miniscript/js/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ export type DescriptorPkType = "derivable" | "definite" | "string";
88

99
export type ScriptContext = "tap" | "segwitv0" | "legacy";
1010

11+
export type SignPsbtResult = {
12+
[inputIndex: number]: [pubkey: string][];
13+
};
14+
1115
declare module "./wasm/wasm_miniscript" {
1216
interface WrapDescriptor {
1317
/** These are not the same types of nodes as in the ast module */
@@ -27,6 +31,10 @@ declare module "./wasm/wasm_miniscript" {
2731
function fromString(miniscript: string, ctx: ScriptContext): WrapMiniscript;
2832
function fromBitcoinScript(script: Uint8Array, ctx: ScriptContext): WrapMiniscript;
2933
}
34+
35+
interface WrapPsbt {
36+
signWithXprv(this: WrapPsbt, xprv: string): SignPsbtResult;
37+
}
3038
}
3139

3240
export { WrapDescriptor as Descriptor } from "./wasm/wasm_miniscript";

packages/wasm-miniscript/src/psbt.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
use crate::descriptor::WrapDescriptorEnum;
2+
use crate::try_into_js_value::TryIntoJsValue;
23
use crate::WrapDescriptor;
34
use miniscript::bitcoin::secp256k1::Secp256k1;
45
use miniscript::bitcoin::Psbt;
56
use miniscript::psbt::PsbtExt;
7+
use std::str::FromStr;
68
use wasm_bindgen::prelude::wasm_bindgen;
7-
use wasm_bindgen::JsError;
9+
use wasm_bindgen::{JsError, JsValue};
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: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use js_sys::Array;
22
use miniscript::bitcoin::hashes::{hash160, ripemd160};
3+
use miniscript::bitcoin::psbt::{SigningKeys, SigningKeysMap};
34
use miniscript::bitcoin::{PublicKey, XOnlyPublicKey};
45
use miniscript::descriptor::{DescriptorType, ShInner, SortedMultiVec, TapTree, Tr, WshInner};
56
use miniscript::{
@@ -257,3 +258,31 @@ impl TryIntoJsValue for DescriptorType {
257258
Ok(JsValue::from_str(&str_from_enum))
258259
}
259260
}
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(
280+
&obj,
281+
&key.to_string().into(),
282+
&value.try_to_js_value()?.into(),
283+
)
284+
.map_err(|_| JsError::new("Failed to set object property"))?;
285+
}
286+
Ok(obj.into())
287+
}
288+
}

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)