diff --git a/packages/wasm-miniscript/js/index.ts b/packages/wasm-miniscript/js/index.ts index 9cdeec8..b722514 100644 --- a/packages/wasm-miniscript/js/index.ts +++ b/packages/wasm-miniscript/js/index.ts @@ -8,6 +8,10 @@ export type DescriptorPkType = "derivable" | "definite" | "string"; export type ScriptContext = "tap" | "segwitv0" | "legacy"; +export type SignPsbtResult = { + [inputIndex: number]: [pubkey: string][]; +}; + declare module "./wasm/wasm_miniscript" { interface WrapDescriptor { /** These are not the same types of nodes as in the ast module */ @@ -27,6 +31,10 @@ declare module "./wasm/wasm_miniscript" { function fromString(miniscript: string, ctx: ScriptContext): WrapMiniscript; function fromBitcoinScript(script: Uint8Array, ctx: ScriptContext): WrapMiniscript; } + + interface WrapPsbt { + signWithXprv(this: WrapPsbt, xprv: string): SignPsbtResult; + } } export { WrapDescriptor as Descriptor } from "./wasm/wasm_miniscript"; diff --git a/packages/wasm-miniscript/src/psbt.rs b/packages/wasm-miniscript/src/psbt.rs index 1dcf2e9..a2e9d9b 100644 --- a/packages/wasm-miniscript/src/psbt.rs +++ b/packages/wasm-miniscript/src/psbt.rs @@ -1,10 +1,12 @@ use crate::descriptor::WrapDescriptorEnum; +use crate::try_into_js_value::TryIntoJsValue; use crate::WrapDescriptor; use miniscript::bitcoin::secp256k1::Secp256k1; use miniscript::bitcoin::Psbt; use miniscript::psbt::PsbtExt; +use std::str::FromStr; use wasm_bindgen::prelude::wasm_bindgen; -use wasm_bindgen::JsError; +use wasm_bindgen::{JsError, JsValue}; #[wasm_bindgen] pub struct WrapPsbt(Psbt); @@ -63,6 +65,16 @@ impl WrapPsbt { } } + #[wasm_bindgen(js_name = signWithXprv)] + pub fn sign_with_xprv(&mut self, xprv: String) -> Result { + let key = miniscript::bitcoin::bip32::Xpriv::from_str(&xprv) + .map_err(|_| JsError::new("Invalid xprv"))?; + self.0 + .sign(&key, &Secp256k1::new()) + .map_err(|(_, errors)| JsError::new(&format!("{} errors: {:?}", errors.len(), errors))) + .and_then(|r| r.try_to_js_value()) + } + #[wasm_bindgen(js_name = finalize)] pub fn finalize_mut(&mut self) -> Result<(), JsError> { self.0 diff --git a/packages/wasm-miniscript/src/try_into_js_value.rs b/packages/wasm-miniscript/src/try_into_js_value.rs index 345e454..b58ffd2 100644 --- a/packages/wasm-miniscript/src/try_into_js_value.rs +++ b/packages/wasm-miniscript/src/try_into_js_value.rs @@ -1,5 +1,6 @@ use js_sys::Array; use miniscript::bitcoin::hashes::{hash160, ripemd160}; +use miniscript::bitcoin::psbt::{SigningKeys, SigningKeysMap}; use miniscript::bitcoin::{PublicKey, XOnlyPublicKey}; use miniscript::descriptor::{DescriptorType, ShInner, SortedMultiVec, TapTree, Tr, WshInner}; use miniscript::{ @@ -256,3 +257,31 @@ impl TryIntoJsValue for DescriptorType { Ok(JsValue::from_str(&str_from_enum)) } } + +impl TryIntoJsValue for SigningKeys { + fn try_to_js_value(&self) -> Result { + match self { + SigningKeys::Ecdsa(v) => { + js_obj!("Ecdsa" => v) + } + SigningKeys::Schnorr(v) => { + js_obj!("Schnorr" => v) + } + } + } +} + +impl TryIntoJsValue for SigningKeysMap { + fn try_to_js_value(&self) -> Result { + let obj = js_sys::Object::new(); + for (key, value) in self.iter() { + js_sys::Reflect::set( + &obj, + &key.to_string().into(), + &value.try_to_js_value()?.into(), + ) + .map_err(|_| JsError::new("Failed to set object property"))?; + } + Ok(obj.into()) + } +} diff --git a/packages/wasm-miniscript/test/psbt.ts b/packages/wasm-miniscript/test/psbt.ts index 5af1b3e..0191dae 100644 --- a/packages/wasm-miniscript/test/psbt.ts +++ b/packages/wasm-miniscript/test/psbt.ts @@ -5,6 +5,7 @@ import { Descriptor, Psbt } from "../js"; import { getDescriptorForScriptType } from "./descriptorUtil"; import { assertEqualPsbt, toUtxoPsbt, toWrappedPsbt, updateInputWithDescriptor } from "./psbt.util"; +import { getKey } from "@bitgo/utxo-lib/dist/src/testutil"; const rootWalletKeys = new utxolib.bitgo.RootWalletKeys(utxolib.testutil.getKeyTriple("wasm")); @@ -14,16 +15,6 @@ function assertEqualBuffer(a: Buffer | Uint8Array, b: Buffer | Uint8Array, messa 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, @@ -40,12 +31,21 @@ function describeUpdateInputWithDescriptor( const index = 0; const descriptor = Descriptor.fromString(descriptorStr, "derivable"); + function getWrappedPsbt() { + return toWrappedPsbt(psbt); + } + + function getWrappedPsbtWithDescriptorInfo(): Psbt { + const wrappedPsbt = getWrappedPsbt(); + const descriptorAtDerivation = descriptor.atDerivationIndex(index); + wrappedPsbt.updateInputWithDescriptor(0, descriptorAtDerivation); + wrappedPsbt.updateOutputWithDescriptor(0, descriptorAtDerivation); + return wrappedPsbt; + } + describe("Wrapped PSBT updateInputWithDescriptor", function () { it("should update the input with the descriptor", function () { - const wrappedPsbt = toWrappedPsbt(psbt); - const descriptorAtDerivation = descriptor.atDerivationIndex(index); - wrappedPsbt.updateInputWithDescriptor(0, descriptorAtDerivation); - wrappedPsbt.updateOutputWithDescriptor(0, descriptorAtDerivation); + const wrappedPsbt = getWrappedPsbtWithDescriptorInfo(); const updatedPsbt = toUtxoPsbt(wrappedPsbt); assertEqualPsbt(updatedPsbt, getFixtureAtStage("unsigned").psbt); updatedPsbt.signAllInputsHD(rootWalletKeys.triple[0]); @@ -85,6 +85,44 @@ function describeUpdateInputWithDescriptor( ); }); }); + + describe("psbt signWithXprv", function () { + type KeyName = utxolib.bitgo.KeyName | "unrelated"; + function signWithKey(keys: KeyName[], { checkFinalized = false } = {}) { + it(`signs the input with keys ${keys}`, function () { + const psbt = getWrappedPsbtWithDescriptorInfo(); + keys.forEach((keyName) => { + const key = keyName === "unrelated" ? getKey(keyName) : rootWalletKeys[keyName]; + const derivationPaths = toUtxoPsbt(psbt).data.inputs[0].bip32Derivation.map( + (d) => d.path, + ); + assert.ok(derivationPaths.every((p) => p === derivationPaths[0])); + const derived = key.derivePath(derivationPaths[0]); + assert.deepStrictEqual(psbt.signWithXprv(key.toBase58()), { + // map: input index -> pubkey array + 0: { Ecdsa: keyName === "unrelated" ? [] : [derived.publicKey.toString("hex")] }, + }); + }); + + if (checkFinalized) { + psbt.finalize(); + assertEqualBuffer( + toUtxoPsbt(psbt).extractTransaction().toBuffer(), + getFixtureAtStage("fullsigned") + .psbt.finalizeAllInputs() + .extractTransaction() + .toBuffer(), + ); + } + }); + } + + signWithKey(["user"]); + signWithKey(["backup"]); + signWithKey(["bitgo"]); + signWithKey(["unrelated"]); + signWithKey(["user", "bitgo"], { checkFinalized: true }); + }); } fixtures.forEach(({ psbt, scriptType, stage }) => {