Skip to content

Commit 752c5e7

Browse files
Merge pull request #61 from BitGo/BTC-1826.add-fromStringDetectType
feat(miniscript): add descriptor type detection and custom error
2 parents 40beb2b + 1de8e99 commit 752c5e7

File tree

8 files changed

+270
-84
lines changed

8 files changed

+270
-84
lines changed

packages/wasm-miniscript/js/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ declare module "./wasm/wasm_miniscript" {
2020

2121
namespace WrapDescriptor {
2222
function fromString(descriptor: string, pkType: DescriptorPkType): WrapDescriptor;
23+
function fromStringDetectType(descriptor: string): WrapDescriptor;
2324
}
2425

2526
interface WrapMiniscript {
Lines changed: 133 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1+
use crate::error::WasmMiniscriptError;
12
use crate::try_into_js_value::TryIntoJsValue;
2-
use miniscript::bitcoin::secp256k1::Secp256k1;
3+
use miniscript::bitcoin::secp256k1::{Context, Secp256k1, Signing};
34
use miniscript::bitcoin::ScriptBuf;
45
use miniscript::descriptor::KeyMap;
56
use miniscript::{DefiniteDescriptorKey, Descriptor, DescriptorPublicKey};
67
use std::str::FromStr;
7-
use wasm_bindgen::prelude::wasm_bindgen;
8-
use wasm_bindgen::{JsError, JsValue};
8+
use wasm_bindgen::prelude::*;
99

1010
pub(crate) enum WrapDescriptorEnum {
1111
Derivable(Descriptor<DescriptorPublicKey>, KeyMap),
@@ -18,7 +18,7 @@ pub struct WrapDescriptor(pub(crate) WrapDescriptorEnum);
1818

1919
#[wasm_bindgen]
2020
impl WrapDescriptor {
21-
pub fn node(&self) -> Result<JsValue, JsError> {
21+
pub fn node(&self) -> Result<JsValue, WasmMiniscriptError> {
2222
Ok(match &self.0 {
2323
WrapDescriptorEnum::Derivable(desc, _) => desc.try_to_js_value()?,
2424
WrapDescriptorEnum::Definite(desc) => desc.try_to_js_value()?,
@@ -45,18 +45,20 @@ impl WrapDescriptor {
4545
}
4646

4747
#[wasm_bindgen(js_name = atDerivationIndex)]
48-
pub fn at_derivation_index(&self, index: u32) -> Result<WrapDescriptor, JsError> {
48+
pub fn at_derivation_index(&self, index: u32) -> Result<WrapDescriptor, WasmMiniscriptError> {
4949
match &self.0 {
5050
WrapDescriptorEnum::Derivable(desc, _keys) => {
5151
let d = desc.at_derivation_index(index)?;
5252
Ok(WrapDescriptor(WrapDescriptorEnum::Definite(d)))
5353
}
54-
_ => Err(JsError::new("Cannot derive from a definite descriptor")),
54+
_ => Err(WasmMiniscriptError::new(
55+
"Cannot derive from a definite descriptor",
56+
)),
5557
}
5658
}
5759

5860
#[wasm_bindgen(js_name = descType)]
59-
pub fn desc_type(&self) -> Result<JsValue, JsError> {
61+
pub fn desc_type(&self) -> Result<JsValue, WasmMiniscriptError> {
6062
(match &self.0 {
6163
WrapDescriptorEnum::Derivable(desc, _) => desc.desc_type(),
6264
WrapDescriptorEnum::Definite(desc) => desc.desc_type(),
@@ -66,34 +68,38 @@ impl WrapDescriptor {
6668
}
6769

6870
#[wasm_bindgen(js_name = scriptPubkey)]
69-
pub fn script_pubkey(&self) -> Result<Vec<u8>, JsError> {
71+
pub fn script_pubkey(&self) -> Result<Vec<u8>, WasmMiniscriptError> {
7072
match &self.0 {
7173
WrapDescriptorEnum::Definite(desc) => Ok(desc.script_pubkey().to_bytes()),
72-
_ => Err(JsError::new("Cannot derive from a non-definite descriptor")),
74+
_ => Err(WasmMiniscriptError::new(
75+
"Cannot encode a derivable descriptor",
76+
)),
7377
}
7478
}
7579

76-
fn explicit_script(&self) -> Result<ScriptBuf, JsError> {
80+
fn explicit_script(&self) -> Result<ScriptBuf, WasmMiniscriptError> {
7781
match &self.0 {
7882
WrapDescriptorEnum::Definite(desc) => Ok(desc.explicit_script()?),
79-
WrapDescriptorEnum::Derivable(_, _) => {
80-
Err(JsError::new("Cannot encode a derivable descriptor"))
81-
}
82-
WrapDescriptorEnum::String(_) => Err(JsError::new("Cannot encode a string descriptor")),
83+
WrapDescriptorEnum::Derivable(_, _) => Err(WasmMiniscriptError::new(
84+
"Cannot encode a derivable descriptor",
85+
)),
86+
WrapDescriptorEnum::String(_) => Err(WasmMiniscriptError::new(
87+
"Cannot encode a string descriptor",
88+
)),
8389
}
8490
}
8591

86-
pub fn encode(&self) -> Result<Vec<u8>, JsError> {
92+
pub fn encode(&self) -> Result<Vec<u8>, WasmMiniscriptError> {
8793
Ok(self.explicit_script()?.to_bytes())
8894
}
8995

9096
#[wasm_bindgen(js_name = toAsmString)]
91-
pub fn to_asm_string(&self) -> Result<String, JsError> {
97+
pub fn to_asm_string(&self) -> Result<String, WasmMiniscriptError> {
9298
Ok(self.explicit_script()?.to_asm_string())
9399
}
94100

95101
#[wasm_bindgen(js_name = maxWeightToSatisfy)]
96-
pub fn max_weight_to_satisfy(&self) -> Result<u32, JsError> {
102+
pub fn max_weight_to_satisfy(&self) -> Result<u32, WasmMiniscriptError> {
97103
let weight = (match &self.0 {
98104
WrapDescriptorEnum::Derivable(desc, _) => desc.max_weight_to_satisfy(),
99105
WrapDescriptorEnum::Definite(desc) => desc.max_weight_to_satisfy(),
@@ -102,26 +108,124 @@ impl WrapDescriptor {
102108
weight
103109
.to_wu()
104110
.try_into()
105-
.map_err(|_| JsError::new("Weight exceeds u32"))
111+
.map_err(|_| WasmMiniscriptError::new("Weight exceeds u32"))
112+
}
113+
114+
fn from_string_derivable<C: Signing>(
115+
secp: &Secp256k1<C>,
116+
descriptor: &str,
117+
) -> Result<WrapDescriptor, WasmMiniscriptError> {
118+
let (desc, keys) = Descriptor::parse_descriptor(&secp, descriptor)?;
119+
Ok(WrapDescriptor(WrapDescriptorEnum::Derivable(desc, keys)))
120+
}
121+
122+
fn from_string_definite(descriptor: &str) -> Result<WrapDescriptor, WasmMiniscriptError> {
123+
let desc = Descriptor::<DefiniteDescriptorKey>::from_str(descriptor)?;
124+
Ok(WrapDescriptor(WrapDescriptorEnum::Definite(desc)))
106125
}
107126

127+
/// Parse a descriptor string with an explicit public key type.
128+
///
129+
/// Note that this function permits parsing a non-derivable descriptor with a derivable key type.
130+
/// Use `from_string_detect_type` to automatically detect the key type.
131+
///
132+
/// # Arguments
133+
/// * `descriptor` - A string containing the descriptor to parse
134+
/// * `pk_type` - The type of public key to expect:
135+
/// - "derivable": For descriptors containing derivation paths (eg. xpubs)
136+
/// - "definite": For descriptors with fully specified keys
137+
/// - "string": For descriptors with string placeholders
138+
///
139+
/// # Returns
140+
/// * `Result<WrapDescriptor, WasmMiniscriptError>` - The parsed descriptor or an error
141+
///
142+
/// # Example
143+
/// ```
144+
/// let desc = WrapDescriptor::from_string(
145+
/// "pk(xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8/*)",
146+
/// "derivable"
147+
/// );
148+
/// ```
108149
#[wasm_bindgen(js_name = fromString, skip_typescript)]
109-
pub fn from_string(descriptor: &str, pk_type: &str) -> Result<WrapDescriptor, JsError> {
150+
pub fn from_string(
151+
descriptor: &str,
152+
pk_type: &str,
153+
) -> Result<WrapDescriptor, WasmMiniscriptError> {
110154
match pk_type {
111-
"derivable" => {
112-
let secp = Secp256k1::new();
113-
let (desc, keys) = Descriptor::parse_descriptor(&secp, descriptor)?;
114-
Ok(WrapDescriptor(WrapDescriptorEnum::Derivable(desc, keys)))
115-
}
116-
"definite" => {
117-
let desc = Descriptor::<DefiniteDescriptorKey>::from_str(descriptor)?;
118-
Ok(WrapDescriptor(WrapDescriptorEnum::Definite(desc)))
119-
}
155+
"derivable" => WrapDescriptor::from_string_derivable(&Secp256k1::new(), descriptor),
156+
"definite" => WrapDescriptor::from_string_definite(descriptor),
120157
"string" => {
121158
let desc = Descriptor::<String>::from_str(descriptor)?;
122159
Ok(WrapDescriptor(WrapDescriptorEnum::String(desc)))
123160
}
124-
_ => Err(JsError::new("Invalid descriptor type")),
161+
_ => Err(WasmMiniscriptError::new("Invalid descriptor type")),
162+
}
163+
}
164+
165+
/// Parse a descriptor string, automatically detecting the appropriate public key type.
166+
/// This will check if the descriptor contains wildcards to determine if it should be
167+
/// parsed as derivable or definite.
168+
///
169+
/// # Arguments
170+
/// * `descriptor` - A string containing the descriptor to parse
171+
///
172+
/// # Returns
173+
/// * `Result<WrapDescriptor, WasmMiniscriptError>` - The parsed descriptor or an error
174+
///
175+
/// # Example
176+
/// ```
177+
/// // Will be parsed as definite since it has no wildcards
178+
/// let desc = WrapDescriptor::from_string_detect_type(
179+
/// "pk(02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5)"
180+
/// );
181+
///
182+
/// // Will be parsed as derivable since it contains a wildcard (*)
183+
/// let desc = WrapDescriptor::from_string_detect_type(
184+
/// "pk(xpub.../0/*)"
185+
/// );
186+
/// ```
187+
#[wasm_bindgen(js_name = fromStringDetectType, skip_typescript)]
188+
pub fn from_string_detect_type(
189+
descriptor: &str,
190+
) -> Result<WrapDescriptor, WasmMiniscriptError> {
191+
let secp = Secp256k1::new();
192+
let (descriptor, _key_map) = Descriptor::parse_descriptor(&secp, descriptor)
193+
.map_err(|_| WasmMiniscriptError::new("Invalid descriptor"))?;
194+
if descriptor.has_wildcard() {
195+
WrapDescriptor::from_string_derivable(&secp, &descriptor.to_string())
196+
} else {
197+
WrapDescriptor::from_string_definite(&descriptor.to_string())
125198
}
126199
}
127200
}
201+
202+
impl FromStr for WrapDescriptor {
203+
type Err = WasmMiniscriptError;
204+
fn from_str(s: &str) -> Result<Self, Self::Err> {
205+
WrapDescriptor::from_string_detect_type(s)
206+
}
207+
}
208+
209+
#[cfg(test)]
210+
mod tests {
211+
use crate::WrapDescriptor;
212+
213+
#[test]
214+
fn test_detect_type() {
215+
let desc = WrapDescriptor::from_string_detect_type(
216+
"pk(L4rK1yDtCWekvXuE6oXD9jCYfFNV2cWRpVuPLBcCU2z8TrisoyY1)",
217+
)
218+
.unwrap();
219+
220+
assert_eq!(desc.has_wildcard(), false);
221+
assert_eq!(
222+
match desc {
223+
WrapDescriptor {
224+
0: crate::descriptor::WrapDescriptorEnum::Definite(_),
225+
} => true,
226+
_ => false,
227+
},
228+
true
229+
);
230+
}
231+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
use core::fmt;
2+
3+
#[derive(Debug, Clone)]
4+
pub enum WasmMiniscriptError {
5+
StringError(String),
6+
}
7+
8+
impl std::error::Error for WasmMiniscriptError {}
9+
impl fmt::Display for WasmMiniscriptError {
10+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
11+
match self {
12+
WasmMiniscriptError::StringError(s) => write!(f, "{}", s),
13+
}
14+
}
15+
}
16+
17+
impl From<&str> for WasmMiniscriptError {
18+
fn from(s: &str) -> Self {
19+
WasmMiniscriptError::StringError(s.to_string())
20+
}
21+
}
22+
23+
impl From<String> for WasmMiniscriptError {
24+
fn from(s: String) -> Self {
25+
WasmMiniscriptError::StringError(s)
26+
}
27+
}
28+
29+
impl From<miniscript::Error> for WasmMiniscriptError {
30+
fn from(err: miniscript::Error) -> Self {
31+
WasmMiniscriptError::StringError(err.to_string())
32+
}
33+
}
34+
35+
impl From<miniscript::descriptor::ConversionError> for WasmMiniscriptError {
36+
fn from(err: miniscript::descriptor::ConversionError) -> Self {
37+
WasmMiniscriptError::StringError(err.to_string())
38+
}
39+
}
40+
41+
impl WasmMiniscriptError {
42+
pub fn new(s: &str) -> WasmMiniscriptError {
43+
WasmMiniscriptError::StringError(s.to_string())
44+
}
45+
}

packages/wasm-miniscript/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
mod descriptor;
2+
mod error;
23
mod miniscript;
34
mod psbt;
45
mod try_into_js_value;

packages/wasm-miniscript/src/miniscript.rs

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1+
use crate::error::WasmMiniscriptError;
2+
use crate::try_into_js_value::TryIntoJsValue;
13
use miniscript::bitcoin::{PublicKey, XOnlyPublicKey};
24
use miniscript::{bitcoin, Legacy, Miniscript, Segwitv0, Tap};
35
use std::str::FromStr;
46
use wasm_bindgen::prelude::wasm_bindgen;
5-
use wasm_bindgen::{JsError, JsValue};
6-
7-
use crate::try_into_js_value::TryIntoJsValue;
7+
use wasm_bindgen::JsValue;
88

99
// Define the macro to simplify operations on WrapMiniscriptEnum variants
1010
// apply a func to the miniscript variant
@@ -30,7 +30,7 @@ pub struct WrapMiniscript(WrapMiniscriptEnum);
3030
#[wasm_bindgen]
3131
impl WrapMiniscript {
3232
#[wasm_bindgen(js_name = node)]
33-
pub fn node(&self) -> Result<JsValue, JsError> {
33+
pub fn node(&self) -> Result<JsValue, WasmMiniscriptError> {
3434
unwrap_apply!(&self.0, |ms| ms.try_to_js_value())
3535
}
3636

@@ -45,43 +45,52 @@ impl WrapMiniscript {
4545
}
4646

4747
#[wasm_bindgen(js_name = toAsmString)]
48-
pub fn to_asm_string(&self) -> Result<String, JsError> {
48+
pub fn to_asm_string(&self) -> Result<String, WasmMiniscriptError> {
4949
unwrap_apply!(&self.0, |ms| Ok(ms.encode().to_asm_string()))
5050
}
5151

5252
#[wasm_bindgen(js_name = fromString, skip_typescript)]
53-
pub fn from_string(script: &str, context_type: &str) -> Result<WrapMiniscript, JsError> {
53+
pub fn from_string(
54+
script: &str,
55+
context_type: &str,
56+
) -> Result<WrapMiniscript, WasmMiniscriptError> {
5457
match context_type {
5558
"tap" => Ok(WrapMiniscript::from(
56-
Miniscript::<XOnlyPublicKey, Tap>::from_str(script).map_err(JsError::from)?,
59+
Miniscript::<XOnlyPublicKey, Tap>::from_str(script)
60+
.map_err(WasmMiniscriptError::from)?,
5761
)),
5862
"segwitv0" => Ok(WrapMiniscript::from(
59-
Miniscript::<PublicKey, Segwitv0>::from_str(script).map_err(JsError::from)?,
63+
Miniscript::<PublicKey, Segwitv0>::from_str(script)
64+
.map_err(WasmMiniscriptError::from)?,
6065
)),
6166
"legacy" => Ok(WrapMiniscript::from(
62-
Miniscript::<PublicKey, Legacy>::from_str(script).map_err(JsError::from)?,
67+
Miniscript::<PublicKey, Legacy>::from_str(script)
68+
.map_err(WasmMiniscriptError::from)?,
6369
)),
64-
_ => Err(JsError::new("Invalid context type")),
70+
_ => Err(WasmMiniscriptError::new("Invalid context type")),
6571
}
6672
}
6773

6874
#[wasm_bindgen(js_name = fromBitcoinScript, skip_typescript)]
6975
pub fn from_bitcoin_script(
7076
script: &[u8],
7177
context_type: &str,
72-
) -> Result<WrapMiniscript, JsError> {
78+
) -> Result<WrapMiniscript, WasmMiniscriptError> {
7379
let script = bitcoin::Script::from_bytes(script);
7480
match context_type {
7581
"tap" => Ok(WrapMiniscript::from(
76-
Miniscript::<XOnlyPublicKey, Tap>::parse(script).map_err(JsError::from)?,
82+
Miniscript::<XOnlyPublicKey, Tap>::parse(script)
83+
.map_err(WasmMiniscriptError::from)?,
7784
)),
7885
"segwitv0" => Ok(WrapMiniscript::from(
79-
Miniscript::<PublicKey, Segwitv0>::parse(script).map_err(JsError::from)?,
86+
Miniscript::<PublicKey, Segwitv0>::parse(script)
87+
.map_err(WasmMiniscriptError::from)?,
8088
)),
8189
"legacy" => Ok(WrapMiniscript::from(
82-
Miniscript::<PublicKey, Legacy>::parse(script).map_err(JsError::from)?,
90+
Miniscript::<PublicKey, Legacy>::parse(script)
91+
.map_err(WasmMiniscriptError::from)?,
8392
)),
84-
_ => Err(JsError::new("Invalid context type")),
93+
_ => Err(WasmMiniscriptError::new("Invalid context type")),
8594
}
8695
}
8796
}

0 commit comments

Comments
 (0)