From d5b41bc1b2af66b40ad633f5c1634d3c4c9f67e0 Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Tue, 16 Jul 2024 11:04:50 +0200 Subject: [PATCH] feat: improve wrapper structs * add to `type Descriptor`: - `hasWildcard` method - `atDerivationIndex` method - `toAsmString` method * add to `type Miniscript`: - `toAsmString` method * add parameter `pkType` to `descriptorFromString` function Issue: BTC-1317 --- .gitignore | 1 + packages/wasm-miniscript/Cargo.lock | 4 +- packages/wasm-miniscript/Cargo.toml | 2 +- packages/wasm-miniscript/js/index.ts | 33 ++++- packages/wasm-miniscript/src/lib.rs | 184 ++++++++++++++++++++++--- packages/wasm-miniscript/src/traits.rs | 72 ++++++---- packages/wasm-miniscript/test/test.ts | 16 ++- packages/wasm-miniscript/tsconfig.json | 7 +- 8 files changed, 257 insertions(+), 62 deletions(-) diff --git a/.gitignore b/.gitignore index c2669b8..e5c48de 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules/ .idea/ *.iml +*.tsbuildinfo diff --git a/packages/wasm-miniscript/Cargo.lock b/packages/wasm-miniscript/Cargo.lock index 8fab2af..ee6eb34 100644 --- a/packages/wasm-miniscript/Cargo.lock +++ b/packages/wasm-miniscript/Cargo.lock @@ -122,9 +122,9 @@ checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "miniscript" -version = "12.0.0" +version = "12.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b59c67956fd276ceec0cf194fbf80754ef4d88a496d5cf5e4fdf33561466183d" +checksum = "64b01e2f1e8e4a5037b61f97de1619c7b2d3f6398aeaddbea9dd16ce9f5a9b33" dependencies = [ "bech32", "bitcoin", diff --git a/packages/wasm-miniscript/Cargo.toml b/packages/wasm-miniscript/Cargo.toml index 2e77052..9bed76d 100644 --- a/packages/wasm-miniscript/Cargo.toml +++ b/packages/wasm-miniscript/Cargo.toml @@ -9,4 +9,4 @@ crate-type = ["cdylib"] [dependencies] wasm-bindgen = "0.2" js-sys = "0.3" -miniscript = "12" +miniscript = { version = "12.1.0", features = ["no-std"] } diff --git a/packages/wasm-miniscript/js/index.ts b/packages/wasm-miniscript/js/index.ts index bbd16ad..d64782c 100644 --- a/packages/wasm-miniscript/js/index.ts +++ b/packages/wasm-miniscript/js/index.ts @@ -1,14 +1,23 @@ import * as wasm from "./wasm/wasm_miniscript"; -type MiniscriptNode = unknown; +// we need to access the wasm module here, otherwise webpack gets all weird +// and forgets to include it in the bundle +void wasm; -type Miniscript = { +export type MiniscriptNode = unknown; + +export type Miniscript = { node(): MiniscriptNode; toString(): string; encode(): Uint8Array; + toAsmString(): string; }; -type ScriptContext = "tap" | "segwitv0" | "legacy"; +export function isMiniscript(obj: unknown): obj is Miniscript { + return obj instanceof wasm.WrapMiniscript; +} + +export type ScriptContext = "tap" | "segwitv0" | "legacy"; export function miniscriptFromString(script: string, scriptContext: ScriptContext): Miniscript { return wasm.miniscript_from_string(script, scriptContext); @@ -21,13 +30,23 @@ export function miniscriptFromBitcoinScript( return wasm.miniscript_from_bitcoin_script(script, scriptContext); } -type DescriptorNode = unknown; +export type DescriptorNode = unknown; -type Descriptor = { +export type Descriptor = { node(): DescriptorNode; toString(): string; + hasWildcard(): boolean; + atDerivationIndex(index: number): Descriptor; + encode(): Uint8Array; + toAsmString(): string; }; -export function descriptorFromString(descriptor: string): Descriptor { - return wasm.descriptor_from_string(descriptor); +export function isDescriptor(obj: unknown): obj is Descriptor { + return obj instanceof wasm.WrapDescriptor; +} + +type DescriptorPkType = "derivable" | "definite" | "string"; + +export function descriptorFromString(descriptor: string, pkType: DescriptorPkType): Descriptor { + return wasm.descriptor_from_string(descriptor, pkType); } diff --git a/packages/wasm-miniscript/src/lib.rs b/packages/wasm-miniscript/src/lib.rs index 104ad57..4f457e0 100644 --- a/packages/wasm-miniscript/src/lib.rs +++ b/packages/wasm-miniscript/src/lib.rs @@ -1,10 +1,44 @@ mod traits; +use crate::traits::TryIntoJsValue; +use miniscript::bitcoin::secp256k1::Secp256k1; +use miniscript::bitcoin::{PublicKey, ScriptBuf}; +use miniscript::descriptor::KeyMap; +use miniscript::{ + bitcoin, bitcoin::XOnlyPublicKey, DefiniteDescriptorKey, Descriptor, DescriptorPublicKey, + Legacy, Miniscript, Segwitv0, Tap, +}; +use std::fmt; use std::str::FromStr; -use miniscript::{bitcoin, bitcoin::XOnlyPublicKey, Descriptor, Legacy, Miniscript, Segwitv0, Tap}; -use miniscript::bitcoin::PublicKey; use wasm_bindgen::prelude::*; -use crate::traits::TryIntoJsValue; + +#[derive(Debug, Clone)] +enum WrapError { + Miniscript(String), + Bitcoin(String), +} + +impl std::error::Error for WrapError {} +impl fmt::Display for WrapError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + WrapError::Miniscript(e) => write!(f, "Miniscript error: {}", e), + WrapError::Bitcoin(e) => write!(f, "Bitcoin error: {}", e), + } + } +} + +impl From for WrapError { + fn from(e: miniscript::Error) -> Self { + WrapError::Miniscript(e.to_string()) + } +} + +impl From for WrapError { + fn from(e: bitcoin::consensus::encode::Error) -> Self { + WrapError::Bitcoin(e.to_string()) + } +} fn wrap_err(r: Result) -> Result { r.map_err(|e| JsValue::from_str(&format!("{:?}", e))) @@ -34,7 +68,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()) } @@ -47,6 +81,11 @@ impl WrapMiniscript { pub fn encode(&self) -> Vec { unwrap_apply!(&self.0, |ms| ms.encode().into_bytes()) } + + #[wasm_bindgen(js_name = toAsmString)] + pub fn to_asm_string(&self) -> Result { + unwrap_apply!(&self.0, |ms| Ok(ms.encode().to_asm_string())) + } } impl From> for WrapMiniscript { @@ -70,42 +109,145 @@ impl From> for WrapMiniscript { #[wasm_bindgen] pub fn miniscript_from_string(script: &str, context_type: &str) -> Result { match context_type { - "tap" => Ok(WrapMiniscript::from(wrap_err(Miniscript::::from_str(script))?)), - "segwitv0" => Ok(WrapMiniscript::from(wrap_err(Miniscript::::from_str(script))?)), - "legacy" => Ok(WrapMiniscript::from(wrap_err(Miniscript::::from_str(script))?)), - _ => Err(JsValue::from_str("Invalid context type")) + "tap" => Ok(WrapMiniscript::from(wrap_err(Miniscript::< + XOnlyPublicKey, + Tap, + >::from_str(script))?)), + "segwitv0" => Ok(WrapMiniscript::from(wrap_err(Miniscript::< + PublicKey, + Segwitv0, + >::from_str(script))?)), + "legacy" => Ok(WrapMiniscript::from(wrap_err(Miniscript::< + PublicKey, + Legacy, + >::from_str(script))?)), + _ => Err(JsValue::from_str("Invalid context type")), } } #[wasm_bindgen] -pub fn miniscript_from_bitcoin_script(script: &[u8], context_type: &str) -> Result { +pub fn miniscript_from_bitcoin_script( + script: &[u8], + context_type: &str, +) -> Result { let script = bitcoin::Script::from_bytes(script); match context_type { - "tap" => Ok(WrapMiniscript::from(wrap_err(Miniscript::::parse(script))?)), - "segwitv0" => Ok(WrapMiniscript::from(wrap_err(Miniscript::::parse(script))?)), - "legacy" => Ok(WrapMiniscript::from(wrap_err(Miniscript::::parse(script))?)), - _ => Err(JsValue::from_str("Invalid context type")) + "tap" => Ok(WrapMiniscript::from(wrap_err(Miniscript::< + XOnlyPublicKey, + Tap, + >::parse(script))?)), + "segwitv0" => Ok(WrapMiniscript::from(wrap_err(Miniscript::< + PublicKey, + Segwitv0, + >::parse(script))?)), + "legacy" => Ok(WrapMiniscript::from(wrap_err(Miniscript::< + PublicKey, + Legacy, + >::parse(script))?)), + _ => Err(JsValue::from_str("Invalid context type")), } } +enum WrapDescriptorEnum { + Derivable(Descriptor, KeyMap), + Definite(Descriptor), + String(Descriptor), +} + #[wasm_bindgen] -pub struct WrapDescriptor(Descriptor); +pub struct WrapDescriptor(WrapDescriptorEnum); #[wasm_bindgen] impl WrapDescriptor { - pub fn node(&self) -> Result { - self.0.try_to_js_value() + 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()?, + WrapDescriptorEnum::String(desc) => desc.try_to_js_value()?, + }) } #[wasm_bindgen(js_name = toString)] pub fn to_string(&self) -> String { - self.0.to_string() + match &self.0 { + WrapDescriptorEnum::Derivable(desc, _) => desc.to_string(), + WrapDescriptorEnum::Definite(desc) => desc.to_string(), + WrapDescriptorEnum::String(desc) => desc.to_string(), + } + } + + #[wasm_bindgen(js_name = hasWildcard)] + pub fn has_wildcard(&self) -> bool { + match &self.0 { + WrapDescriptorEnum::Derivable(desc, _) => desc.has_wildcard(), + WrapDescriptorEnum::Definite(_) => false, + WrapDescriptorEnum::String(_) => false, + } + } + + #[wasm_bindgen(js_name = atDerivationIndex)] + 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")), + } + } + + 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")), + } + } + + pub fn encode(&self) -> Result, JsError> { + Ok(self.explicit_script()?.to_bytes()) + } + + #[wasm_bindgen(js_name = toAsmString)] + pub fn to_asm_string(&self) -> Result { + Ok(self.explicit_script()?.to_asm_string()) } } #[wasm_bindgen] -pub fn descriptor_from_string(descriptor: &str) -> Result { - Ok( - WrapDescriptor(Descriptor::::from_str(descriptor) - .map_err(|e| js_sys::Error::new(&format!("{:?}", e)))?)) +pub fn descriptor_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))) + } + "string" => { + let desc = Descriptor::::from_str(descriptor)?; + Ok(WrapDescriptor(WrapDescriptorEnum::String(desc))) + } + _ => Err(JsError::new("Invalid descriptor type")), + } } + + +#[test] +pub fn panic_xprv() { + + let (d,m) = Descriptor::parse_descriptor( + &Secp256k1::new(), + "wsh(multi(2,xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U/2147483647'/0,xprv9vHkqa6EV4sPZHYqZznhT2NPtPCjKuDKGY38FBWLvgaDx45zo9WQRUT3dKYnjwih2yJD9mkrocEZXo1ex8G81dwSM1fwqWpWkeS3v86pgKt/1/2/*,xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi/10/20/30/40/*'))", + ).unwrap(); + + let dd = d.at_derivation_index(0).unwrap(); + + let _ = dd.explicit_script().unwrap(); +} \ No newline at end of file diff --git a/packages/wasm-miniscript/src/traits.rs b/packages/wasm-miniscript/src/traits.rs index c65dafe..037c2fa 100644 --- a/packages/wasm-miniscript/src/traits.rs +++ b/packages/wasm-miniscript/src/traits.rs @@ -1,22 +1,23 @@ -use wasm_bindgen::JsValue; +use wasm_bindgen::{JsError, JsValue}; use js_sys::Array; -use miniscript::{AbsLockTime, Descriptor, hash256, Miniscript, MiniscriptKey, RelLockTime, ScriptContext, Terminal, Threshold}; +use miniscript::{AbsLockTime, DefiniteDescriptorKey, Descriptor, DescriptorPublicKey, hash256, Miniscript, MiniscriptKey, RelLockTime, ScriptContext, Terminal, Threshold}; use miniscript::descriptor::{ShInner, SortedMultiVec, TapTree, Tr, WshInner}; use miniscript::bitcoin::{PublicKey, XOnlyPublicKey}; use miniscript::bitcoin::hashes::{hash160, ripemd160}; use std::sync::Arc; pub(crate) trait TryIntoJsValue { - fn try_to_js_value(&self) -> Result; + fn try_to_js_value(&self) -> Result; } macro_rules! js_obj { ( $( $key:expr => $value:expr ),* ) => {{ let obj = js_sys::Object::new(); $( - js_sys::Reflect::set(&obj, &$key.into(), &$value.try_to_js_value()?.into())?; + js_sys::Reflect::set(&obj, &$key.into(), &$value.try_to_js_value()?.into()) + .map_err(|_| JsError::new("Failed to set object property"))?; )* - Ok(Into::::into(obj)) as Result + Ok(Into::::into(obj)) as Result }}; } @@ -31,26 +32,26 @@ macro_rules! js_arr { } 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()?); @@ -60,7 +61,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) @@ -69,55 +70,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_str(&self.to_string())) } } 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() { @@ -128,13 +129,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), @@ -169,7 +170,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(), @@ -179,7 +180,7 @@ impl TryIntoJsValue for } 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()), @@ -190,7 +191,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), @@ -199,7 +200,7 @@ impl TryIntoJsValue for WshInner { } impl TryIntoJsValue for Tr { - fn try_to_js_value(&self) -> Result { + fn try_to_js_value(&self) -> Result { js_obj!( "internalKey" => self.internal_key(), "tree" => self.tap_tree() @@ -208,7 +209,7 @@ impl TryIntoJsValue for Tr { } 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() @@ -216,8 +217,25 @@ impl TryIntoJsValue for TapTree { } } +impl TryIntoJsValue for DescriptorPublicKey { + 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()), + DescriptorPublicKey::MultiXPub(_v) => js_obj!("MultiXPub" => self.to_string()), + } + } +} + +impl TryIntoJsValue for DefiniteDescriptorKey { + 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()), diff --git a/packages/wasm-miniscript/test/test.ts b/packages/wasm-miniscript/test/test.ts index 9f5c3da..7938056 100644 --- a/packages/wasm-miniscript/test/test.ts +++ b/packages/wasm-miniscript/test/test.ts @@ -20,7 +20,7 @@ function removeChecksum(descriptor: string): string { describe("Descriptor fixtures", function () { fixtures.valid.forEach((fixture, i) => { it("should parse fixture " + i, function () { - const descriptor = descriptorFromString(fixture.descriptor); + const descriptor = descriptorFromString(fixture.descriptor, "string"); assert.doesNotThrow(() => descriptor.node()); let descriptorString = descriptor.toString(); if (fixture.checksumRequired === false) { @@ -32,6 +32,20 @@ describe("Descriptor fixtures", function () { expected = expected.replace("and_n", "and_b"); } assert.strictEqual(descriptorString, expected); + + assert.doesNotThrow(() => + descriptorFromString(fixture.descriptor, "derivable").atDerivationIndex(0), + ); + + const nonDerivable = [33, 34, 35, 41, 42, 43]; + if (nonDerivable.includes(i)) { + // FIXME(BTC-1337): xprvs with hardened derivations are not supported yet + console.log("Skipping encoding test for fixture", fixture.descriptor, i); + } else { + assert.doesNotThrow(() => + descriptorFromString(fixture.descriptor, "derivable").atDerivationIndex(0).encode(), + ); + } }); }); }); diff --git a/packages/wasm-miniscript/tsconfig.json b/packages/wasm-miniscript/tsconfig.json index ee05a3e..7996220 100644 --- a/packages/wasm-miniscript/tsconfig.json +++ b/packages/wasm-miniscript/tsconfig.json @@ -6,8 +6,9 @@ "esModuleInterop": true, "allowJs": true, "skipLibCheck": true, - "declaration": true + "declaration": true, + "outDir": "dist" }, - "include": ["./js/**/*"], - "exclude": ["node_modules", "./js/wasm/**/*", "./js/browser/**/*"] + "include": ["./js/index.ts"], + "exclude": ["node_modules", "./js/wasm/**/*"] }