Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
321 changes: 139 additions & 182 deletions packages/wasm-utxo/Cargo.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/wasm-utxo/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ all = "warn"
[dependencies]
wasm-bindgen = "0.2"
js-sys = "0.3"
miniscript = { git = "https://github.com/BitGo/rust-miniscript", tag = "miniscript-12.3.4-opdrop" }
miniscript = { git = "https://github.com/BitGo/rust-miniscript", tag = "miniscript-13.0.0-opdrop" }
bech32 = "0.11"
musig2 = { version = "0.3.1", default-features = false, features = ["k256"] }
getrandom = { version = "0.2", features = ["js"] }
Expand Down
4 changes: 3 additions & 1 deletion packages/wasm-utxo/deny.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
multiple-versions = "deny"
# Highlight bech32 specifically (ensures it's caught if duplicated)
highlight = "all"
# Skip hex-conservative duplicate (miniscript uses 1.0.1 directly, bitcoin uses 0.2.2 via bitcoin_hashes)
skip = [{ crate = "hex-conservative@0.2.2" }]

# Allow git sources (needed for miniscript)
[sources]
Expand All @@ -13,9 +15,9 @@ allow-git = ["https://github.com/BitGo/rust-miniscript"]
allow = [
"MIT",
"Apache-2.0",
"Apache-2.0 WITH LLVM-exception",
"CC0-1.0",
"MITNFA",
"Unicode-DFS-2016",
"Unicode-3.0",
"BSD-3-Clause",
"Unlicense",
Expand Down
4 changes: 2 additions & 2 deletions packages/wasm-utxo/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ impl From<miniscript::Error> for WasmUtxoError {
}
}

impl From<miniscript::descriptor::ConversionError> for WasmUtxoError {
fn from(err: miniscript::descriptor::ConversionError) -> Self {
impl From<miniscript::descriptor::NonDefiniteKeyError> for WasmUtxoError {
fn from(err: miniscript::descriptor::NonDefiniteKeyError) -> Self {
WasmUtxoError::StringError(err.to_string())
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ impl ScriptP2tr {
}

pub fn output_script(&self) -> ScriptBuf {
let output_key = self.spend_info.output_key().to_inner();
let output_key = self.spend_info.output_key().to_x_only_public_key();

Builder::new()
.push_int(1)
Expand Down
6 changes: 3 additions & 3 deletions packages/wasm-utxo/src/wasm/miniscript.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,13 +75,13 @@ impl WrapMiniscript {
let script = bitcoin::Script::from_bytes(script);
match context_type {
"tap" => Ok(WrapMiniscript::from(
Miniscript::<XOnlyPublicKey, Tap>::parse(script).map_err(WasmUtxoError::from)?,
Miniscript::<XOnlyPublicKey, Tap>::decode(script).map_err(WasmUtxoError::from)?,
)),
"segwitv0" => Ok(WrapMiniscript::from(
Miniscript::<PublicKey, Segwitv0>::parse(script).map_err(WasmUtxoError::from)?,
Miniscript::<PublicKey, Segwitv0>::decode(script).map_err(WasmUtxoError::from)?,
)),
"legacy" => Ok(WrapMiniscript::from(
Miniscript::<PublicKey, Legacy>::parse(script).map_err(WasmUtxoError::from)?,
Miniscript::<PublicKey, Legacy>::decode(script).map_err(WasmUtxoError::from)?,
)),
_ => Err(WasmUtxoError::new("Invalid context type")),
}
Expand Down
1 change: 1 addition & 0 deletions packages/wasm-utxo/src/wasm/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ mod ecpair;
mod fixed_script_wallet;
mod miniscript;
mod psbt;
mod recursive_tap_tree;
mod replay_protection;
mod try_from_js_value;
mod try_into_js_value;
Expand Down
66 changes: 66 additions & 0 deletions packages/wasm-utxo/src/wasm/recursive_tap_tree.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
use miniscript::descriptor::TapTree;
use miniscript::{Miniscript, MiniscriptKey};

use crate::error::WasmUtxoError;

/// The recursive tap tree was removed from rust-miniscript in https://github.com/rust-bitcoin/rust-miniscript/pull/808
/// Our API is somewhat dependent on it and providing backwards compatibility is easier than rewriting everything.
pub enum RecursiveTapTree<Pk: MiniscriptKey> {
Tree {
left: Box<RecursiveTapTree<Pk>>,
right: Box<RecursiveTapTree<Pk>>,
},
Leaf(Miniscript<Pk, miniscript::Tap>),
}

impl<Pk: MiniscriptKey + Clone> TryFrom<&TapTree<Pk>> for RecursiveTapTree<Pk> {
type Error = WasmUtxoError;

fn try_from(tree: &TapTree<Pk>) -> Result<Self, Self::Error> {
use std::sync::Arc;

// Collect leaves with depths (miniscript() returns Arc<Miniscript>)
let leaves: Vec<(u8, Arc<Miniscript<Pk, miniscript::Tap>>)> = tree
.leaves()
.map(|item| (item.depth(), item.miniscript().clone()))
.collect();

if leaves.is_empty() {
return Err(WasmUtxoError::new("Empty tap tree"));
}

// Stack-based reconstruction: process leaves left-to-right,
// combining siblings at the same depth into Tree nodes
let mut stack: Vec<(u8, RecursiveTapTree<Pk>)> = Vec::new();

for (depth, ms) in leaves {
// Clone the Miniscript from the Arc
stack.push((depth, RecursiveTapTree::Leaf((*ms).clone())));

// Combine nodes at the same depth
while stack.len() >= 2 {
let len = stack.len();
if stack[len - 2].0 != stack[len - 1].0 {
break;
}

let (_, right) = stack.pop().unwrap();
let (d, left) = stack.pop().unwrap();

stack.push((
d - 1,
RecursiveTapTree::Tree {
left: Box::new(left),
right: Box::new(right),
},
));
}
}

if stack.len() != 1 {
return Err(WasmUtxoError::new("Invalid tap tree structure"));
}

Ok(stack.pop().unwrap().1)
}
}
23 changes: 19 additions & 4 deletions packages/wasm-utxo/src/wasm/try_into_js_value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -235,19 +235,34 @@ impl<Pk: MiniscriptKey + TryIntoJsValue> TryIntoJsValue for WshInner<Pk> {

impl<Pk: MiniscriptKey + TryIntoJsValue> TryIntoJsValue for Tr<Pk> {
fn try_to_js_value(&self) -> Result<JsValue, WasmUtxoError> {
Ok(js_arr!(self.internal_key(), self.tap_tree()))
let tap_tree_js: JsValue = match self.tap_tree() {
Some(tree) => tree.try_to_js_value()?,
None => JsValue::NULL,
};
Ok(js_arr!(self.internal_key(), tap_tree_js))
}
}

impl<Pk: MiniscriptKey + TryIntoJsValue> TryIntoJsValue for TapTree<Pk> {
use super::recursive_tap_tree::RecursiveTapTree;

impl<Pk: MiniscriptKey + TryIntoJsValue> TryIntoJsValue for RecursiveTapTree<Pk> {
fn try_to_js_value(&self) -> Result<JsValue, WasmUtxoError> {
match self {
TapTree::Tree { left, right, .. } => js_obj!("Tree" => js_arr!(left, right)),
TapTree::Leaf(ms) => ms.try_to_js_value(),
RecursiveTapTree::Tree { left, right } => js_obj!("Tree" => js_arr!(left, right)),
RecursiveTapTree::Leaf(ms) => ms.try_to_js_value(),
}
}
}

impl<Pk: MiniscriptKey + TryIntoJsValue> TryIntoJsValue for TapTree<Pk>
where
Pk: Clone,
{
fn try_to_js_value(&self) -> Result<JsValue, WasmUtxoError> {
RecursiveTapTree::try_from(self)?.try_to_js_value()
}
}

impl TryIntoJsValue for DescriptorPublicKey {
fn try_to_js_value(&self) -> Result<JsValue, WasmUtxoError> {
match self {
Expand Down
6 changes: 3 additions & 3 deletions packages/wasm-utxo/test/fixtures/2.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
{
"descriptor": "pkh(03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd)#wf36a0pg",
"descriptor": "pkh([deadbeef/1/2'/3/4']03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd)#v0p5w8jl",
"wasmNode": {
"Pkh": {
"Single": "03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd"
"Single": "[deadbeef/1/2'/3/4']03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd"
}
},
"ast": {
"pkh": "03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd"
"pkh": "[deadbeef/1/2'/3/4']03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd"
}
}