From da670930a0ac9a54a196ab97b54e3eaee514f6d3 Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Wed, 26 Feb 2025 11:51:03 +0100 Subject: [PATCH 1/2] feat(miniscript): add custom error type for wasm-miniscript Replaces JsError with WasmMiniscriptError. This allows writing tests for funcs that return `Result<_, WasmMiniscriptError>`. Previously it was a pain because `JsError` did not implement Debug. Issue: BTC-1826 --- packages/wasm-miniscript/src/descriptor.rs | 69 ++++++++++-------- packages/wasm-miniscript/src/error.rs | 45 ++++++++++++ packages/wasm-miniscript/src/lib.rs | 1 + packages/wasm-miniscript/src/miniscript.rs | 39 ++++++---- packages/wasm-miniscript/src/psbt.rs | 24 ++++--- .../wasm-miniscript/src/try_into_js_value.rs | 71 ++++++++++--------- packages/wasm-miniscript/test/test.ts | 7 ++ 7 files changed, 172 insertions(+), 84 deletions(-) create mode 100644 packages/wasm-miniscript/src/error.rs diff --git a/packages/wasm-miniscript/src/descriptor.rs b/packages/wasm-miniscript/src/descriptor.rs index d0e068a..67ad1dd 100644 --- a/packages/wasm-miniscript/src/descriptor.rs +++ b/packages/wasm-miniscript/src/descriptor.rs @@ -1,11 +1,11 @@ +use crate::error::WasmMiniscriptError; use crate::try_into_js_value::TryIntoJsValue; use miniscript::bitcoin::secp256k1::Secp256k1; use miniscript::bitcoin::ScriptBuf; use miniscript::descriptor::KeyMap; use miniscript::{DefiniteDescriptorKey, Descriptor, DescriptorPublicKey}; use std::str::FromStr; -use wasm_bindgen::prelude::wasm_bindgen; -use wasm_bindgen::{JsError, JsValue}; +use wasm_bindgen::prelude::*; pub(crate) enum WrapDescriptorEnum { Derivable(Descriptor, KeyMap), @@ -18,7 +18,7 @@ pub struct WrapDescriptor(pub(crate) WrapDescriptorEnum); #[wasm_bindgen] impl WrapDescriptor { - pub fn node(&self) -> Result { + pub fn node(&self) -> Result { Ok(match &self.0 { WrapDescriptorEnum::Derivable(desc, _) => desc.try_to_js_value()?, WrapDescriptorEnum::Definite(desc) => desc.try_to_js_value()?, @@ -45,18 +45,20 @@ impl WrapDescriptor { } #[wasm_bindgen(js_name = atDerivationIndex)] - pub fn at_derivation_index(&self, index: u32) -> Result { + pub fn at_derivation_index(&self, index: u32) -> Result { match &self.0 { WrapDescriptorEnum::Derivable(desc, _keys) => { let d = desc.at_derivation_index(index)?; Ok(WrapDescriptor(WrapDescriptorEnum::Definite(d))) } - _ => Err(JsError::new("Cannot derive from a definite descriptor")), + _ => Err(WasmMiniscriptError::new( + "Cannot derive from a definite descriptor", + )), } } #[wasm_bindgen(js_name = descType)] - pub fn desc_type(&self) -> Result { + pub fn desc_type(&self) -> Result { (match &self.0 { WrapDescriptorEnum::Derivable(desc, _) => desc.desc_type(), WrapDescriptorEnum::Definite(desc) => desc.desc_type(), @@ -66,34 +68,36 @@ impl WrapDescriptor { } #[wasm_bindgen(js_name = scriptPubkey)] - pub fn script_pubkey(&self) -> Result, JsError> { + pub fn script_pubkey(&self) -> Result, WasmMiniscriptError> { match &self.0 { WrapDescriptorEnum::Definite(desc) => Ok(desc.script_pubkey().to_bytes()), - _ => Err(JsError::new("Cannot derive from a non-definite descriptor")), + _ => Err(WasmMiniscriptError::new("Cannot derive from a non-definite descriptor")), } } - fn explicit_script(&self) -> Result { + fn explicit_script(&self) -> Result { match &self.0 { WrapDescriptorEnum::Definite(desc) => Ok(desc.explicit_script()?), - WrapDescriptorEnum::Derivable(_, _) => { - Err(JsError::new("Cannot encode a derivable descriptor")) - } - WrapDescriptorEnum::String(_) => Err(JsError::new("Cannot encode a string descriptor")), + WrapDescriptorEnum::Derivable(_, _) => Err(WasmMiniscriptError::new( + "Cannot encode a derivable descriptor", + )), + WrapDescriptorEnum::String(_) => Err(WasmMiniscriptError::new( + "Cannot encode a string descriptor", + )), } } - pub fn encode(&self) -> Result, JsError> { + pub fn encode(&self) -> Result, WasmMiniscriptError> { Ok(self.explicit_script()?.to_bytes()) } #[wasm_bindgen(js_name = toAsmString)] - pub fn to_asm_string(&self) -> Result { + pub fn to_asm_string(&self) -> Result { Ok(self.explicit_script()?.to_asm_string()) } #[wasm_bindgen(js_name = maxWeightToSatisfy)] - pub fn max_weight_to_satisfy(&self) -> Result { + pub fn max_weight_to_satisfy(&self) -> Result { let weight = (match &self.0 { WrapDescriptorEnum::Derivable(desc, _) => desc.max_weight_to_satisfy(), WrapDescriptorEnum::Definite(desc) => desc.max_weight_to_satisfy(), @@ -102,26 +106,33 @@ impl WrapDescriptor { weight .to_wu() .try_into() - .map_err(|_| JsError::new("Weight exceeds u32")) + .map_err(|_| WasmMiniscriptError::new("Weight exceeds u32")) + } + + fn from_string_derivable(descriptor: &str) -> Result { + let secp = Secp256k1::new(); + let (desc, keys) = Descriptor::parse_descriptor(&secp, descriptor)?; + Ok(WrapDescriptor(WrapDescriptorEnum::Derivable(desc, keys))) + } + + fn from_string_definite(descriptor: &str) -> Result { + let desc = Descriptor::::from_str(descriptor)?; + Ok(WrapDescriptor(WrapDescriptorEnum::Definite(desc))) } #[wasm_bindgen(js_name = fromString, skip_typescript)] - pub fn from_string(descriptor: &str, pk_type: &str) -> Result { + pub fn from_string( + descriptor: &str, + pk_type: &str, + ) -> Result { match pk_type { - "derivable" => { - let secp = Secp256k1::new(); - let (desc, keys) = Descriptor::parse_descriptor(&secp, descriptor)?; - Ok(WrapDescriptor(WrapDescriptorEnum::Derivable(desc, keys))) - } - "definite" => { - let desc = Descriptor::::from_str(descriptor)?; - Ok(WrapDescriptor(WrapDescriptorEnum::Definite(desc))) - } + "derivable" => WrapDescriptor::from_string_derivable(descriptor), + "definite" => WrapDescriptor::from_string_definite(descriptor), "string" => { let desc = Descriptor::::from_str(descriptor)?; Ok(WrapDescriptor(WrapDescriptorEnum::String(desc))) } - _ => Err(JsError::new("Invalid descriptor type")), + _ => Err(WasmMiniscriptError::new("Invalid descriptor type")), } } -} +} \ No newline at end of file diff --git a/packages/wasm-miniscript/src/error.rs b/packages/wasm-miniscript/src/error.rs new file mode 100644 index 0000000..402fe20 --- /dev/null +++ b/packages/wasm-miniscript/src/error.rs @@ -0,0 +1,45 @@ +use core::fmt; + +#[derive(Debug, Clone)] +pub enum WasmMiniscriptError { + StringError(String), +} + +impl std::error::Error for WasmMiniscriptError {} +impl fmt::Display for WasmMiniscriptError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + WasmMiniscriptError::StringError(s) => write!(f, "{}", s), + } + } +} + +impl From<&str> for WasmMiniscriptError { + fn from(s: &str) -> Self { + WasmMiniscriptError::StringError(s.to_string()) + } +} + +impl From for WasmMiniscriptError { + fn from(s: String) -> Self { + WasmMiniscriptError::StringError(s) + } +} + +impl From for WasmMiniscriptError { + fn from(err: miniscript::Error) -> Self { + WasmMiniscriptError::StringError(err.to_string()) + } +} + +impl From for WasmMiniscriptError { + fn from(err: miniscript::descriptor::ConversionError) -> Self { + WasmMiniscriptError::StringError(err.to_string()) + } +} + +impl WasmMiniscriptError { + pub fn new(s: &str) -> WasmMiniscriptError { + WasmMiniscriptError::StringError(s.to_string()) + } +} diff --git a/packages/wasm-miniscript/src/lib.rs b/packages/wasm-miniscript/src/lib.rs index 40047b9..3003a8e 100644 --- a/packages/wasm-miniscript/src/lib.rs +++ b/packages/wasm-miniscript/src/lib.rs @@ -1,4 +1,5 @@ mod descriptor; +mod error; mod miniscript; mod psbt; mod try_into_js_value; diff --git a/packages/wasm-miniscript/src/miniscript.rs b/packages/wasm-miniscript/src/miniscript.rs index c173f1a..f068cd1 100644 --- a/packages/wasm-miniscript/src/miniscript.rs +++ b/packages/wasm-miniscript/src/miniscript.rs @@ -1,10 +1,10 @@ +use crate::error::WasmMiniscriptError; +use crate::try_into_js_value::TryIntoJsValue; use miniscript::bitcoin::{PublicKey, XOnlyPublicKey}; use miniscript::{bitcoin, Legacy, Miniscript, Segwitv0, Tap}; use std::str::FromStr; use wasm_bindgen::prelude::wasm_bindgen; -use wasm_bindgen::{JsError, JsValue}; - -use crate::try_into_js_value::TryIntoJsValue; +use wasm_bindgen::JsValue; // Define the macro to simplify operations on WrapMiniscriptEnum variants // apply a func to the miniscript variant @@ -30,7 +30,7 @@ pub struct WrapMiniscript(WrapMiniscriptEnum); #[wasm_bindgen] impl WrapMiniscript { #[wasm_bindgen(js_name = node)] - pub fn node(&self) -> Result { + pub fn node(&self) -> Result { unwrap_apply!(&self.0, |ms| ms.try_to_js_value()) } @@ -45,23 +45,29 @@ impl WrapMiniscript { } #[wasm_bindgen(js_name = toAsmString)] - pub fn to_asm_string(&self) -> Result { + pub fn to_asm_string(&self) -> Result { unwrap_apply!(&self.0, |ms| Ok(ms.encode().to_asm_string())) } #[wasm_bindgen(js_name = fromString, skip_typescript)] - pub fn from_string(script: &str, context_type: &str) -> Result { + pub fn from_string( + script: &str, + context_type: &str, + ) -> Result { match context_type { "tap" => Ok(WrapMiniscript::from( - Miniscript::::from_str(script).map_err(JsError::from)?, + Miniscript::::from_str(script) + .map_err(WasmMiniscriptError::from)?, )), "segwitv0" => Ok(WrapMiniscript::from( - Miniscript::::from_str(script).map_err(JsError::from)?, + Miniscript::::from_str(script) + .map_err(WasmMiniscriptError::from)?, )), "legacy" => Ok(WrapMiniscript::from( - Miniscript::::from_str(script).map_err(JsError::from)?, + Miniscript::::from_str(script) + .map_err(WasmMiniscriptError::from)?, )), - _ => Err(JsError::new("Invalid context type")), + _ => Err(WasmMiniscriptError::new("Invalid context type")), } } @@ -69,19 +75,22 @@ impl WrapMiniscript { pub fn from_bitcoin_script( script: &[u8], context_type: &str, - ) -> Result { + ) -> Result { let script = bitcoin::Script::from_bytes(script); match context_type { "tap" => Ok(WrapMiniscript::from( - Miniscript::::parse(script).map_err(JsError::from)?, + Miniscript::::parse(script) + .map_err(WasmMiniscriptError::from)?, )), "segwitv0" => Ok(WrapMiniscript::from( - Miniscript::::parse(script).map_err(JsError::from)?, + Miniscript::::parse(script) + .map_err(WasmMiniscriptError::from)?, )), "legacy" => Ok(WrapMiniscript::from( - Miniscript::::parse(script).map_err(JsError::from)?, + Miniscript::::parse(script) + .map_err(WasmMiniscriptError::from)?, )), - _ => Err(JsError::new("Invalid context type")), + _ => Err(WasmMiniscriptError::new("Invalid context type")), } } } diff --git a/packages/wasm-miniscript/src/psbt.rs b/packages/wasm-miniscript/src/psbt.rs index 0c845b5..0bf7971 100644 --- a/packages/wasm-miniscript/src/psbt.rs +++ b/packages/wasm-miniscript/src/psbt.rs @@ -1,4 +1,5 @@ use crate::descriptor::WrapDescriptorEnum; +use crate::error::WasmMiniscriptError; use crate::try_into_js_value::TryIntoJsValue; use crate::WrapDescriptor; use miniscript::bitcoin::bip32::Fingerprint; @@ -131,30 +132,37 @@ impl WrapPsbt { } #[wasm_bindgen(js_name = signWithXprv)] - pub fn sign_with_xprv(&mut self, xprv: String) -> Result { - let key = bip32::Xpriv::from_str(&xprv).map_err(|_| JsError::new("Invalid xprv"))?; + pub fn sign_with_xprv(&mut self, xprv: String) -> Result { + let key = + bip32::Xpriv::from_str(&xprv).map_err(|_| WasmMiniscriptError::new("Invalid xprv"))?; self.0 .sign(&key, &Secp256k1::new()) - .map_err(|(_, errors)| JsError::new(&format!("{} errors: {:?}", errors.len(), errors))) + .map_err(|(_, errors)| { + WasmMiniscriptError::new(&format!("{} errors: {:?}", errors.len(), errors)) + }) .and_then(|r| r.try_to_js_value()) } #[wasm_bindgen(js_name = signWithPrv)] - pub fn sign_with_prv(&mut self, prv: Vec) -> Result { + pub fn sign_with_prv(&mut self, prv: Vec) -> Result { let privkey = PrivateKey::from_slice(&prv, miniscript::bitcoin::network::Network::Bitcoin) - .map_err(|_| JsError::new("Invalid private key"))?; + .map_err(|_| WasmMiniscriptError::new("Invalid private key"))?; let secp = Secp256k1::new(); self.0 .sign(&SingleKeySigner::from_privkey(privkey, &secp), &secp) - .map_err(|(r, errors)| JsError::new(&format!("{} errors: {:?}", errors.len(), errors))) + .map_err(|(r, errors)| { + WasmMiniscriptError::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> { + pub fn finalize_mut(&mut self) -> Result<(), WasmMiniscriptError> { self.0 .finalize_mut(&Secp256k1::verification_only()) - .map_err(|vec_err| JsError::new(&format!("{} errors: {:?}", vec_err.len(), vec_err))) + .map_err(|vec_err| { + WasmMiniscriptError::new(&format!("{} errors: {:?}", vec_err.len(), vec_err)) + }) } } diff --git a/packages/wasm-miniscript/src/try_into_js_value.rs b/packages/wasm-miniscript/src/try_into_js_value.rs index b58ffd2..a763da6 100644 --- a/packages/wasm-miniscript/src/try_into_js_value.rs +++ b/packages/wasm-miniscript/src/try_into_js_value.rs @@ -1,3 +1,4 @@ +use crate::error::WasmMiniscriptError; use js_sys::Array; use miniscript::bitcoin::hashes::{hash160, ripemd160}; use miniscript::bitcoin::psbt::{SigningKeys, SigningKeysMap}; @@ -8,10 +9,10 @@ use miniscript::{ MiniscriptKey, RelLockTime, ScriptContext, Terminal, Threshold, }; use std::sync::Arc; -use wasm_bindgen::{JsError, JsValue}; +use wasm_bindgen::JsValue; pub(crate) trait TryIntoJsValue { - fn try_to_js_value(&self) -> Result; + fn try_to_js_value(&self) -> Result; } macro_rules! js_obj { @@ -19,9 +20,9 @@ macro_rules! js_obj { let obj = js_sys::Object::new(); $( js_sys::Reflect::set(&obj, &$key.into(), &$value.try_to_js_value()?.into()) - .map_err(|_| JsError::new("Failed to set object property"))?; + .map_err(|_| WasmMiniscriptError::new("Failed to set object property"))?; )* - Ok(Into::::into(obj)) as Result + Ok(Into::::into(obj)) as Result }}; } @@ -35,27 +36,33 @@ macro_rules! js_arr { }}; } +impl From for JsValue { + fn from(err: WasmMiniscriptError) -> Self { + js_sys::Error::new(&err.to_string()).into() + } +} + impl TryIntoJsValue for JsValue { - fn try_to_js_value(&self) -> Result { + fn try_to_js_value(&self) -> Result { Ok(self.clone()) } } impl TryIntoJsValue for Arc { - fn try_to_js_value(&self) -> Result { + fn try_to_js_value(&self) -> Result { self.as_ref().try_to_js_value() } } impl TryIntoJsValue for String { - fn try_to_js_value(&self) -> Result { + fn try_to_js_value(&self) -> Result { Ok(JsValue::from_str(self)) } } // array of TryToJsValue impl TryIntoJsValue for Vec { - fn try_to_js_value(&self) -> Result { + fn try_to_js_value(&self) -> Result { let arr = Array::new(); for item in self.iter() { arr.push(&item.try_to_js_value()?); @@ -65,7 +72,7 @@ impl TryIntoJsValue for Vec { } impl TryIntoJsValue for Option { - fn try_to_js_value(&self) -> Result { + fn try_to_js_value(&self) -> Result { match self { Some(v) => v.try_to_js_value(), None => Ok(JsValue::NULL), @@ -74,55 +81,55 @@ impl TryIntoJsValue for Option { } impl TryIntoJsValue for XOnlyPublicKey { - fn try_to_js_value(&self) -> Result { + fn try_to_js_value(&self) -> Result { Ok(JsValue::from_str(&self.to_string())) } } impl TryIntoJsValue for PublicKey { - fn try_to_js_value(&self) -> Result { + fn try_to_js_value(&self) -> Result { Ok(JsValue::from_str(&self.to_string())) } } impl TryIntoJsValue for AbsLockTime { - fn try_to_js_value(&self) -> Result { + fn try_to_js_value(&self) -> Result { Ok(JsValue::from_f64(self.to_consensus_u32() as f64)) } } impl TryIntoJsValue for RelLockTime { - fn try_to_js_value(&self) -> Result { + fn try_to_js_value(&self) -> Result { Ok(JsValue::from_f64(self.to_consensus_u32() as f64)) } } impl TryIntoJsValue for ripemd160::Hash { - fn try_to_js_value(&self) -> Result { + fn try_to_js_value(&self) -> Result { Ok(JsValue::from_str(&self.to_string())) } } impl TryIntoJsValue for hash160::Hash { - fn try_to_js_value(&self) -> Result { + fn try_to_js_value(&self) -> Result { Ok(JsValue::from_str(&self.to_string())) } } impl TryIntoJsValue for hash256::Hash { - fn try_to_js_value(&self) -> Result { + fn try_to_js_value(&self) -> Result { Ok(JsValue::from_str(&self.to_string())) } } impl TryIntoJsValue for usize { - fn try_to_js_value(&self) -> Result { + fn try_to_js_value(&self) -> Result { Ok(JsValue::from_f64(*self as f64)) } } impl TryIntoJsValue for Threshold { - fn try_to_js_value(&self) -> Result { + fn try_to_js_value(&self) -> Result { let arr = Array::new(); arr.push(&self.k().try_to_js_value()?); for v in self.iter() { @@ -135,13 +142,13 @@ impl TryIntoJsValue for Threshold { impl TryIntoJsValue for Miniscript { - fn try_to_js_value(&self) -> Result { + fn try_to_js_value(&self) -> Result { self.node.try_to_js_value() } } impl TryIntoJsValue for Terminal { - fn try_to_js_value(&self) -> Result { + fn try_to_js_value(&self) -> Result { match self { Terminal::True => Ok(JsValue::TRUE), Terminal::False => Ok(JsValue::FALSE), @@ -178,7 +185,7 @@ impl TryIntoJsValue for impl TryIntoJsValue for SortedMultiVec { - fn try_to_js_value(&self) -> Result { + fn try_to_js_value(&self) -> Result { js_obj!( "k" => self.k(), "n" => self.n(), @@ -188,7 +195,7 @@ impl TryIntoJsValue } impl TryIntoJsValue for ShInner { - fn try_to_js_value(&self) -> Result { + fn try_to_js_value(&self) -> Result { match self { ShInner::Wsh(v) => js_obj!("Wsh" => v.as_inner()), ShInner::Wpkh(v) => js_obj!("Wpkh" => v.as_inner()), @@ -199,7 +206,7 @@ impl TryIntoJsValue for ShInner { } impl TryIntoJsValue for WshInner { - fn try_to_js_value(&self) -> Result { + fn try_to_js_value(&self) -> Result { match self { WshInner::SortedMulti(v) => js_obj!("SortedMulti" => v), WshInner::Ms(v) => js_obj!("Ms" => v), @@ -208,13 +215,13 @@ impl TryIntoJsValue for WshInner { } impl TryIntoJsValue for Tr { - fn try_to_js_value(&self) -> Result { + fn try_to_js_value(&self) -> Result { Ok(js_arr!(self.internal_key(), self.tap_tree())) } } impl TryIntoJsValue for TapTree { - fn try_to_js_value(&self) -> Result { + fn try_to_js_value(&self) -> Result { match self { TapTree::Tree { left, right, .. } => js_obj!("Tree" => js_arr!(left, right)), TapTree::Leaf(ms) => ms.try_to_js_value(), @@ -223,7 +230,7 @@ impl TryIntoJsValue for TapTree { } impl TryIntoJsValue for DescriptorPublicKey { - fn try_to_js_value(&self) -> Result { + fn try_to_js_value(&self) -> Result { match self { DescriptorPublicKey::Single(_v) => js_obj!("Single" => self.to_string()), DescriptorPublicKey::XPub(_v) => js_obj!("XPub" => self.to_string()), @@ -233,13 +240,13 @@ impl TryIntoJsValue for DescriptorPublicKey { } impl TryIntoJsValue for DefiniteDescriptorKey { - fn try_to_js_value(&self) -> Result { + fn try_to_js_value(&self) -> Result { self.as_descriptor_public_key().try_to_js_value() } } impl TryIntoJsValue for Descriptor { - fn try_to_js_value(&self) -> Result { + fn try_to_js_value(&self) -> Result { match self { Descriptor::Bare(v) => js_obj!("Bare" => v.as_inner()), Descriptor::Pkh(v) => js_obj!("Pkh" => v.as_inner()), @@ -252,14 +259,14 @@ impl TryIntoJsValue for Descriptor { } impl TryIntoJsValue for DescriptorType { - fn try_to_js_value(&self) -> Result { + fn try_to_js_value(&self) -> Result { let str_from_enum = format!("{:?}", self); Ok(JsValue::from_str(&str_from_enum)) } } impl TryIntoJsValue for SigningKeys { - fn try_to_js_value(&self) -> Result { + fn try_to_js_value(&self) -> Result { match self { SigningKeys::Ecdsa(v) => { js_obj!("Ecdsa" => v) @@ -272,7 +279,7 @@ impl TryIntoJsValue for SigningKeys { } impl TryIntoJsValue for SigningKeysMap { - fn try_to_js_value(&self) -> Result { + fn try_to_js_value(&self) -> Result { let obj = js_sys::Object::new(); for (key, value) in self.iter() { js_sys::Reflect::set( @@ -280,7 +287,7 @@ impl TryIntoJsValue for SigningKeysMap { &key.to_string().into(), &value.try_to_js_value()?.into(), ) - .map_err(|_| JsError::new("Failed to set object property"))?; + .map_err(|_| WasmMiniscriptError::new("Failed to set object property"))?; } Ok(obj.into()) } diff --git a/packages/wasm-miniscript/test/test.ts b/packages/wasm-miniscript/test/test.ts index 034cac1..d696be9 100644 --- a/packages/wasm-miniscript/test/test.ts +++ b/packages/wasm-miniscript/test/test.ts @@ -56,6 +56,13 @@ function assertIsErrorUnknownWrapper(error: unknown, wrapper: string) { } describe("Descriptor fixtures", function () { + it("throws proper error", function () { + assert.throws( + () => Descriptor.fromString("lol", "derivable"), + (err) => err instanceof Error, + ); + }); + fixtures.valid.forEach((fixture, i) => { describe("fixture " + i, function () { const isOpDropFixture = i === 59; From 1de8e994a2c1d398ec0c5e0babe70ade715f71c8 Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Wed, 26 Feb 2025 13:06:03 +0100 Subject: [PATCH 2/2] feat(miniscript): add auto-detection of descriptor type Add fromStringDetectType() that auto-detects whether a descriptor contains wildcards. If it does, returns a derivable descriptor, otherwise a definite one. Issue: BTC-1826 --- packages/wasm-miniscript/js/index.ts | 1 + packages/wasm-miniscript/src/descriptor.rs | 105 +++++++++++++++++++-- packages/wasm-miniscript/test/test.ts | 4 + 3 files changed, 104 insertions(+), 6 deletions(-) diff --git a/packages/wasm-miniscript/js/index.ts b/packages/wasm-miniscript/js/index.ts index 8ff308b..8152113 100644 --- a/packages/wasm-miniscript/js/index.ts +++ b/packages/wasm-miniscript/js/index.ts @@ -20,6 +20,7 @@ declare module "./wasm/wasm_miniscript" { namespace WrapDescriptor { function fromString(descriptor: string, pkType: DescriptorPkType): WrapDescriptor; + function fromStringDetectType(descriptor: string): WrapDescriptor; } interface WrapMiniscript { diff --git a/packages/wasm-miniscript/src/descriptor.rs b/packages/wasm-miniscript/src/descriptor.rs index 67ad1dd..a08ab22 100644 --- a/packages/wasm-miniscript/src/descriptor.rs +++ b/packages/wasm-miniscript/src/descriptor.rs @@ -1,6 +1,6 @@ use crate::error::WasmMiniscriptError; use crate::try_into_js_value::TryIntoJsValue; -use miniscript::bitcoin::secp256k1::Secp256k1; +use miniscript::bitcoin::secp256k1::{Context, Secp256k1, Signing}; use miniscript::bitcoin::ScriptBuf; use miniscript::descriptor::KeyMap; use miniscript::{DefiniteDescriptorKey, Descriptor, DescriptorPublicKey}; @@ -71,7 +71,9 @@ impl WrapDescriptor { pub fn script_pubkey(&self) -> Result, WasmMiniscriptError> { match &self.0 { WrapDescriptorEnum::Definite(desc) => Ok(desc.script_pubkey().to_bytes()), - _ => Err(WasmMiniscriptError::new("Cannot derive from a non-definite descriptor")), + _ => Err(WasmMiniscriptError::new( + "Cannot encode a derivable descriptor", + )), } } @@ -109,8 +111,10 @@ impl WrapDescriptor { .map_err(|_| WasmMiniscriptError::new("Weight exceeds u32")) } - fn from_string_derivable(descriptor: &str) -> Result { - let secp = Secp256k1::new(); + fn from_string_derivable( + secp: &Secp256k1, + descriptor: &str, + ) -> Result { let (desc, keys) = Descriptor::parse_descriptor(&secp, descriptor)?; Ok(WrapDescriptor(WrapDescriptorEnum::Derivable(desc, keys))) } @@ -120,13 +124,35 @@ impl WrapDescriptor { Ok(WrapDescriptor(WrapDescriptorEnum::Definite(desc))) } + /// Parse a descriptor string with an explicit public key type. + /// + /// Note that this function permits parsing a non-derivable descriptor with a derivable key type. + /// Use `from_string_detect_type` to automatically detect the key type. + /// + /// # Arguments + /// * `descriptor` - A string containing the descriptor to parse + /// * `pk_type` - The type of public key to expect: + /// - "derivable": For descriptors containing derivation paths (eg. xpubs) + /// - "definite": For descriptors with fully specified keys + /// - "string": For descriptors with string placeholders + /// + /// # Returns + /// * `Result` - The parsed descriptor or an error + /// + /// # Example + /// ``` + /// let desc = WrapDescriptor::from_string( + /// "pk(xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8/*)", + /// "derivable" + /// ); + /// ``` #[wasm_bindgen(js_name = fromString, skip_typescript)] pub fn from_string( descriptor: &str, pk_type: &str, ) -> Result { match pk_type { - "derivable" => WrapDescriptor::from_string_derivable(descriptor), + "derivable" => WrapDescriptor::from_string_derivable(&Secp256k1::new(), descriptor), "definite" => WrapDescriptor::from_string_definite(descriptor), "string" => { let desc = Descriptor::::from_str(descriptor)?; @@ -135,4 +161,71 @@ impl WrapDescriptor { _ => Err(WasmMiniscriptError::new("Invalid descriptor type")), } } -} \ No newline at end of file + + /// Parse a descriptor string, automatically detecting the appropriate public key type. + /// This will check if the descriptor contains wildcards to determine if it should be + /// parsed as derivable or definite. + /// + /// # Arguments + /// * `descriptor` - A string containing the descriptor to parse + /// + /// # Returns + /// * `Result` - The parsed descriptor or an error + /// + /// # Example + /// ``` + /// // Will be parsed as definite since it has no wildcards + /// let desc = WrapDescriptor::from_string_detect_type( + /// "pk(02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5)" + /// ); + /// + /// // Will be parsed as derivable since it contains a wildcard (*) + /// let desc = WrapDescriptor::from_string_detect_type( + /// "pk(xpub.../0/*)" + /// ); + /// ``` + #[wasm_bindgen(js_name = fromStringDetectType, skip_typescript)] + pub fn from_string_detect_type( + descriptor: &str, + ) -> Result { + let secp = Secp256k1::new(); + let (descriptor, _key_map) = Descriptor::parse_descriptor(&secp, descriptor) + .map_err(|_| WasmMiniscriptError::new("Invalid descriptor"))?; + if descriptor.has_wildcard() { + WrapDescriptor::from_string_derivable(&secp, &descriptor.to_string()) + } else { + WrapDescriptor::from_string_definite(&descriptor.to_string()) + } + } +} + +impl FromStr for WrapDescriptor { + type Err = WasmMiniscriptError; + fn from_str(s: &str) -> Result { + WrapDescriptor::from_string_detect_type(s) + } +} + +#[cfg(test)] +mod tests { + use crate::WrapDescriptor; + + #[test] + fn test_detect_type() { + let desc = WrapDescriptor::from_string_detect_type( + "pk(L4rK1yDtCWekvXuE6oXD9jCYfFNV2cWRpVuPLBcCU2z8TrisoyY1)", + ) + .unwrap(); + + assert_eq!(desc.has_wildcard(), false); + assert_eq!( + match desc { + WrapDescriptor { + 0: crate::descriptor::WrapDescriptorEnum::Definite(_), + } => true, + _ => false, + }, + true + ); + } +} diff --git a/packages/wasm-miniscript/test/test.ts b/packages/wasm-miniscript/test/test.ts index d696be9..9523c2d 100644 --- a/packages/wasm-miniscript/test/test.ts +++ b/packages/wasm-miniscript/test/test.ts @@ -84,6 +84,10 @@ describe("Descriptor fixtures", function () { return; } + it("should detect descriptor type", function () { + Descriptor.fromStringDetectType(fixture.descriptor); + }); + it("should round-trip (pkType string)", function () { let descriptorString = Descriptor.fromString(fixture.descriptor, "string").toString(); if (fixture.checksumRequired === false) {