From ec0b4961372240e7333d74e3675b081cc35cbf9b Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Thu, 30 Oct 2025 10:48:58 +0100 Subject: [PATCH 1/2] feat(wasm-utxo): isolate WASM bindings into separate module Move all WASM-specific code into a dedicated module to improve organization and separation of concerns. The core functionality remains unchanged, but the binding layer is now cleanly separated from the implementation. This refactoring: - Creates a new `wasm` module to contain all bindings - Moves JS/WASM interface code out of core implementation files - Maintains backward compatibility with existing JS API Issue: BTC-2652 Co-authored-by: llm-git --- packages/wasm-utxo/README.md | 5 + packages/wasm-utxo/package.json | 4 +- packages/wasm-utxo/src/address/networks.rs | 36 +---- .../wasm-utxo/src/address/utxolib_compat.rs | 69 --------- .../wasm-utxo/src/fixed_script_wallet/mod.rs | 64 -------- .../src/fixed_script_wallet/wallet_keys.rs | 146 +----------------- packages/wasm-utxo/src/lib.rs | 12 +- packages/wasm-utxo/src/wasm-bindgen.md | 78 +++++++--- packages/wasm-utxo/src/wasm/address.rs | 37 +++++ .../bip32interface.rs | 2 +- .../wasm-utxo/src/{ => wasm}/descriptor.rs | 6 +- .../wasm-utxo/src/wasm/fixed_script_wallet.rs | 66 ++++++++ .../wasm-utxo/src/{ => wasm}/miniscript.rs | 2 +- packages/wasm-utxo/src/wasm/mod.rs | 17 ++ packages/wasm-utxo/src/{ => wasm}/psbt.rs | 8 +- .../src/{ => wasm}/try_from_js_value.rs | 0 .../src/{ => wasm}/try_into_js_value.rs | 0 packages/wasm-utxo/src/wasm/utxolib_compat.rs | 66 ++++++++ .../wasm-utxo/src/wasm/wallet_keys_helpers.rs | 137 ++++++++++++++++ 19 files changed, 408 insertions(+), 347 deletions(-) create mode 100644 packages/wasm-utxo/src/wasm/address.rs rename packages/wasm-utxo/src/{fixed_script_wallet => wasm}/bip32interface.rs (96%) rename packages/wasm-utxo/src/{ => wasm}/descriptor.rs (97%) create mode 100644 packages/wasm-utxo/src/wasm/fixed_script_wallet.rs rename packages/wasm-utxo/src/{ => wasm}/miniscript.rs (98%) create mode 100644 packages/wasm-utxo/src/wasm/mod.rs rename packages/wasm-utxo/src/{ => wasm}/psbt.rs (98%) rename packages/wasm-utxo/src/{ => wasm}/try_from_js_value.rs (100%) rename packages/wasm-utxo/src/{ => wasm}/try_into_js_value.rs (100%) create mode 100644 packages/wasm-utxo/src/wasm/utxolib_compat.rs create mode 100644 packages/wasm-utxo/src/wasm/wallet_keys_helpers.rs diff --git a/packages/wasm-utxo/README.md b/packages/wasm-utxo/README.md index 6c68e774..5c870fca 100644 --- a/packages/wasm-utxo/README.md +++ b/packages/wasm-utxo/README.md @@ -5,6 +5,11 @@ This project is the successor of the Javascript `utxo-lib` package. It provides WASM bindings for the `rust-bitcoin` and `rust-miniscript` crates that help verify and co-sign transactions built by the BitGo Wallet Platform API. +## Documentation + +- **[`src/wasm-bindgen.md`](src/wasm-bindgen.md)** - Guide for creating WASM bindings using the namespace pattern +- **[`js/README.md`](js/README.md)** - TypeScript wrapper layer architecture and best practices + ## Status This project is under active development. diff --git a/packages/wasm-utxo/package.json b/packages/wasm-utxo/package.json index 349e9753..ad5593d3 100644 --- a/packages/wasm-utxo/package.json +++ b/packages/wasm-utxo/package.json @@ -26,7 +26,9 @@ "./dist/node/js/index.js": "./dist/browser/js/index.js" }, "scripts": { - "test": "mocha --recursive test", + "test": "npm run test:mocha && npm run test:wasm-pack", + "test:mocha": "mocha --recursive test", + "test:wasm-pack": "npm run test:wasm-pack-node && npm run test:wasm-pack-chrome", "test:wasm-pack-node": "wasm-pack test --node", "test:wasm-pack-chrome": "wasm-pack test --headless --chrome", "build:wasm": "make js/wasm/ && make dist/node/js/wasm/ && make dist/browser/js/wasm/", diff --git a/packages/wasm-utxo/src/address/networks.rs b/packages/wasm-utxo/src/address/networks.rs index 84c0d746..5d6429de 100644 --- a/packages/wasm-utxo/src/address/networks.rs +++ b/packages/wasm-utxo/src/address/networks.rs @@ -14,6 +14,7 @@ use super::{ }; use crate::bitcoin::Script; use crate::networks::Network; +use miniscript::bitcoin::WitnessVersion; /// Get codecs for decoding addresses for a given network. /// Returns multiple codecs to try in order (Base58Check, Bech32, CashAddr, etc.) @@ -302,41 +303,6 @@ pub fn from_output_script_with_coin_and_format( from_output_script_with_network_and_format(script, network, format) } -use miniscript::bitcoin::WitnessVersion; -// WASM bindings -use wasm_bindgen::prelude::*; - -#[wasm_bindgen] -pub struct AddressNamespace; - -#[wasm_bindgen] -impl AddressNamespace { - #[wasm_bindgen] - pub fn to_output_script_with_coin( - address: &str, - coin: &str, - ) -> std::result::Result, JsValue> { - to_output_script_with_coin(address, coin) - .map(|script| script.to_bytes()) - .map_err(|e| JsValue::from_str(&e.to_string())) - } - - #[wasm_bindgen] - pub fn from_output_script_with_coin( - script: &[u8], - coin: &str, - format: Option, - ) -> std::result::Result { - let script_obj = Script::from_bytes(script); - let format_str = format.as_deref(); - let address_format = AddressFormat::from_optional_str(format_str) - .map_err(|e| JsValue::from_str(&e.to_string()))?; - - from_output_script_with_coin_and_format(script_obj, coin, address_format) - .map_err(|e| JsValue::from_str(&e.to_string())) - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/packages/wasm-utxo/src/address/utxolib_compat.rs b/packages/wasm-utxo/src/address/utxolib_compat.rs index c792225c..8dca22fd 100644 --- a/packages/wasm-utxo/src/address/utxolib_compat.rs +++ b/packages/wasm-utxo/src/address/utxolib_compat.rs @@ -1,8 +1,6 @@ /// Helper structs for compatibility with npm @bitgo/utxo-lib /// Long-term we should not use the `Network` objects from @bitgo/utxo-lib any longer, /// but for now we need to keep this compatibility layer. -use wasm_bindgen::JsValue; - use crate::address::networks::{AddressFormat, OutputScriptSupport}; use crate::address::{bech32, cashaddr, Base58CheckCodec}; use crate::bitcoin::{Script, ScriptBuf}; @@ -30,12 +28,6 @@ pub struct UtxolibNetwork { } impl UtxolibNetwork { - /// Parse a UtxolibNetwork object from a JavaScript value - pub fn from_js_value(js_network: &JsValue) -> Result { - use crate::try_from_js_value::TryFromJsValue; - UtxolibNetwork::try_from_js_value(js_network) - .map_err(|e| AddressError::InvalidAddress(e.to_string())) - } pub fn output_script_support(&self) -> OutputScriptSupport { let segwit = self.bech32.is_some(); @@ -140,64 +132,3 @@ pub fn to_output_script_with_network(address: &str, network: &UtxolibNetwork) -> address ))) } - -// WASM bindings for utxolib-compatible address functions -use wasm_bindgen::prelude::*; - -#[wasm_bindgen] -pub struct UtxolibCompatNamespace; - -#[wasm_bindgen] -impl UtxolibCompatNamespace { - /// Convert output script to address string - /// - /// # Arguments - /// * `script` - The output script as a byte array - /// * `network` - The UtxolibNetwork object from JavaScript - /// * `format` - Optional address format: "default" or "cashaddr" (only applicable for Bitcoin Cash and eCash) - #[wasm_bindgen] - pub fn from_output_script( - script: &[u8], - network: JsValue, - format: Option, - ) -> std::result::Result { - let network = UtxolibNetwork::from_js_value(&network) - .map_err(|e| JsValue::from_str(&e.to_string()))?; - - let script_obj = Script::from_bytes(script); - - let format_str = format.as_deref(); - let address_format = AddressFormat::from_optional_str(format_str) - .map_err(|e| JsValue::from_str(&e.to_string()))?; - - from_output_script_with_network(script_obj, &network, address_format) - .map_err(|e| JsValue::from_str(&e.to_string())) - } - - /// Convert address string to output script - /// - /// # Arguments - /// * `address` - The address string - /// * `network` - The UtxolibNetwork object from JavaScript - /// * `format` - Optional address format (currently unused for decoding as all formats are accepted) - #[wasm_bindgen] - pub fn to_output_script( - address: &str, - network: JsValue, - format: Option, - ) -> std::result::Result, JsValue> { - let network = UtxolibNetwork::from_js_value(&network) - .map_err(|e| JsValue::from_str(&e.to_string()))?; - - // Validate format parameter even though we don't use it for decoding - if let Some(fmt) = format { - let format_str = Some(fmt.as_str()); - AddressFormat::from_optional_str(format_str) - .map_err(|e| JsValue::from_str(&e.to_string()))?; - } - - to_output_script_with_network(address, &network) - .map(|script| script.to_bytes()) - .map_err(|e| JsValue::from_str(&e.to_string())) - } -} diff --git a/packages/wasm-utxo/src/fixed_script_wallet/mod.rs b/packages/wasm-utxo/src/fixed_script_wallet/mod.rs index c85630d1..0c1c6653 100644 --- a/packages/wasm-utxo/src/fixed_script_wallet/mod.rs +++ b/packages/wasm-utxo/src/fixed_script_wallet/mod.rs @@ -1,6 +1,5 @@ /// This module contains code for the BitGo Fixed Script Wallets. /// These are not based on descriptors. -mod bip32interface; mod wallet_keys; pub mod wallet_scripts; @@ -10,66 +9,3 @@ pub mod test_utils; pub use wallet_keys::*; pub use wallet_scripts::*; -use wasm_bindgen::prelude::*; - -use crate::address::networks::AddressFormat; -use crate::error::WasmUtxoError; -use crate::try_from_js_value::TryFromJsValue; -use crate::utxolib_compat::UtxolibNetwork; - -#[wasm_bindgen] -pub struct FixedScriptWalletNamespace; - -#[wasm_bindgen] -impl FixedScriptWalletNamespace { - #[wasm_bindgen] - pub fn output_script( - keys: JsValue, - chain: u32, - index: u32, - network: JsValue, - ) -> Result, WasmUtxoError> { - let network = UtxolibNetwork::try_from_js_value(&network)?; - let chain = Chain::try_from(chain) - .map_err(|e| WasmUtxoError::new(&format!("Invalid chain: {}", e)))?; - - let wallet_keys = RootWalletKeys::from_jsvalue(&keys)?; - let scripts = WalletScripts::from_wallet_keys( - &wallet_keys, - chain, - index, - &network.output_script_support(), - )?; - Ok(scripts.output_script().to_bytes()) - } - - #[wasm_bindgen] - pub fn address( - keys: JsValue, - chain: u32, - index: u32, - network: JsValue, - address_format: Option, - ) -> Result { - let network = UtxolibNetwork::try_from_js_value(&network)?; - let wallet_keys = RootWalletKeys::from_jsvalue(&keys)?; - let chain = Chain::try_from(chain) - .map_err(|e| WasmUtxoError::new(&format!("Invalid chain: {}", e)))?; - let scripts = WalletScripts::from_wallet_keys( - &wallet_keys, - chain, - index, - &network.output_script_support(), - )?; - let script = scripts.output_script(); - let address_format = AddressFormat::from_optional_str(address_format.as_deref()) - .map_err(|e| WasmUtxoError::new(&format!("Invalid address format: {}", e)))?; - let address = crate::address::utxolib_compat::from_output_script_with_network( - &script, - &network, - address_format, - ) - .map_err(|e| WasmUtxoError::new(&format!("Failed to generate address: {}", e)))?; - Ok(address.to_string()) - } -} diff --git a/packages/wasm-utxo/src/fixed_script_wallet/wallet_keys.rs b/packages/wasm-utxo/src/fixed_script_wallet/wallet_keys.rs index 82461d5c..4fbd2450 100644 --- a/packages/wasm-utxo/src/fixed_script_wallet/wallet_keys.rs +++ b/packages/wasm-utxo/src/fixed_script_wallet/wallet_keys.rs @@ -4,36 +4,11 @@ use std::str::FromStr; use crate::bitcoin::bip32::{ChildNumber, DerivationPath}; use crate::bitcoin::{bip32::Xpub, secp256k1::Secp256k1, CompressedPublicKey}; use crate::error::WasmUtxoError; -use wasm_bindgen::JsValue; - -use super::bip32interface::xpub_from_bip32interface; pub type XpubTriple = [Xpub; 3]; pub type PubTriple = [CompressedPublicKey; 3]; -pub fn xpub_triple_from_jsvalue(keys: &JsValue) -> Result { - let keys_array = js_sys::Array::from(keys); - if keys_array.length() != 3 { - return Err(WasmUtxoError::new("Expected exactly 3 xpub keys")); - } - - let key_strings: Result<[String; 3], _> = (0..3) - .map(|i| { - keys_array - .get(i) - .as_string() - .ok_or_else(|| WasmUtxoError::new(&format!("Key at index {} is not a string", i))) - }) - .collect::, _>>() - .and_then(|v| { - v.try_into() - .map_err(|_| WasmUtxoError::new("Failed to convert to array")) - }); - - xpub_triple_from_strings(&key_strings?) -} - pub fn xpub_triple_from_strings(xpub_strings: &[String; 3]) -> Result { let xpubs: Result, _> = xpub_strings .iter() @@ -113,113 +88,6 @@ impl RootWalletKeys { .try_into() .map_err(|_| WasmUtxoError::new("Expected exactly 3 derived xpubs")) } - - pub(crate) fn from_jsvalue(keys: &JsValue) -> Result { - // Check if keys is an array (xpub strings) or an object (WalletKeys/RootWalletKeys) - if js_sys::Array::is_array(keys) { - // Handle array of xpub strings - let xpubs = xpub_triple_from_jsvalue(keys)?; - Ok(RootWalletKeys::new_with_derivation_prefixes( - xpubs, - [ - DerivationPath::from_str("m/0/0").unwrap(), - DerivationPath::from_str("m/0/0").unwrap(), - DerivationPath::from_str("m/0/0").unwrap(), - ], - )) - } else if keys.is_object() { - // Handle WalletKeys/RootWalletKeys object - let obj = js_sys::Object::from(keys.clone()); - - // Get the triple property - let triple = js_sys::Reflect::get(&obj, &JsValue::from_str("triple")) - .map_err(|_| WasmUtxoError::new("Failed to get 'triple' property"))?; - - if !js_sys::Array::is_array(&triple) { - return Err(WasmUtxoError::new("'triple' property must be an array")); - } - - let triple_array = js_sys::Array::from(&triple); - if triple_array.length() != 3 { - return Err(WasmUtxoError::new("'triple' must contain exactly 3 keys")); - } - - // Extract xpubs from BIP32Interface objects - let xpubs: XpubTriple = (0..3) - .map(|i| { - let bip32_key = triple_array.get(i); - xpub_from_bip32interface(&bip32_key) - }) - .collect::, _>>()? - .try_into() - .map_err(|_| WasmUtxoError::new("Failed to convert to array"))?; - - // Try to get derivationPrefixes if present (for RootWalletKeys) - let derivation_prefixes = - js_sys::Reflect::get(&obj, &JsValue::from_str("derivationPrefixes")) - .ok() - .and_then(|prefixes| { - if prefixes.is_undefined() || prefixes.is_null() { - return None; - } - - if !js_sys::Array::is_array(&prefixes) { - return None; - } - - let prefixes_array = js_sys::Array::from(&prefixes); - if prefixes_array.length() != 3 { - return None; - } - - let prefix_strings: Result<[String; 3], _> = (0..3) - .map(|i| { - prefixes_array - .get(i) - .as_string() - .ok_or_else(|| WasmUtxoError::new("Prefix is not a string")) - }) - .collect::, _>>() - .and_then(|v| { - v.try_into() - .map_err(|_| WasmUtxoError::new("Failed to convert to array")) - }); - - prefix_strings.ok() - }); - - // Convert prefix strings to DerivationPath - let derivation_paths = if let Some(prefixes) = derivation_prefixes { - prefixes - .iter() - .map(|p| { - // Remove leading 'm/' if present and add it back - let p = p.strip_prefix("m/").unwrap_or(p); - DerivationPath::from_str(&format!("m/{}", p)).map_err(|e| { - WasmUtxoError::new(&format!("Invalid derivation prefix: {}", e)) - }) - }) - .collect::, _>>()? - .try_into() - .map_err(|_| WasmUtxoError::new("Failed to convert derivation paths"))? - } else { - [ - DerivationPath::from_str("m/0/0").unwrap(), - DerivationPath::from_str("m/0/0").unwrap(), - DerivationPath::from_str("m/0/0").unwrap(), - ] - }; - - Ok(RootWalletKeys::new_with_derivation_prefixes( - xpubs, - derivation_paths, - )) - } else { - Err(WasmUtxoError::new( - "Expected array of xpub strings or WalletKeys object", - )) - } - } } #[cfg(test)] @@ -265,7 +133,7 @@ pub mod tests { pub mod wasm_tests { use super::tests::get_test_wallet_xprvs; use crate::bitcoin::bip32::Xpub; - use crate::RootWalletKeys; + use crate::wasm::wallet_keys_helpers::root_wallet_keys_from_jsvalue; use wasm_bindgen::JsValue; use wasm_bindgen_test::*; @@ -288,7 +156,7 @@ pub mod wasm_tests { } // Test from_jsvalue with actual JsValue - let result = RootWalletKeys::from_jsvalue(&js_array.into()); + let result = root_wallet_keys_from_jsvalue(&js_array.into()); assert!(result.is_ok()); let wallet_keys = result.unwrap(); @@ -309,7 +177,7 @@ pub mod wasm_tests { js_array.push(&JsValue::from_str(&xpub_str)); } - let result = RootWalletKeys::from_jsvalue(&js_array.into()); + let result = root_wallet_keys_from_jsvalue(&js_array.into()); assert!(result.is_err()); assert_eq!( result.unwrap_err().to_string(), @@ -333,7 +201,7 @@ pub mod wasm_tests { &Xpub::from_priv(&secp, &xpubs[0]).to_string(), )); - let result = RootWalletKeys::from_jsvalue(&js_array.into()); + let result = root_wallet_keys_from_jsvalue(&js_array.into()); assert!(result.is_err()); assert_eq!( result.unwrap_err().to_string(), @@ -349,7 +217,7 @@ pub mod wasm_tests { js_array.push(&JsValue::from_str("also-not-valid")); js_array.push(&JsValue::from_str("still-not-valid")); - let result = RootWalletKeys::from_jsvalue(&js_array.into()); + let result = root_wallet_keys_from_jsvalue(&js_array.into()); assert!(result.is_err()); assert!(result .unwrap_err() @@ -365,7 +233,7 @@ pub mod wasm_tests { js_array.push(&JsValue::from_str("xpub2")); js_array.push(&JsValue::from_str("xpub3")); - let result = RootWalletKeys::from_jsvalue(&js_array.into()); + let result = root_wallet_keys_from_jsvalue(&js_array.into()); assert!(result.is_err()); assert!(result .unwrap_err() @@ -381,7 +249,7 @@ pub mod wasm_tests { js_array.push(&JsValue::UNDEFINED); js_array.push(&JsValue::from_bool(true)); - let result = RootWalletKeys::from_jsvalue(&js_array.into()); + let result = root_wallet_keys_from_jsvalue(&js_array.into()); assert!(result.is_err()); } } diff --git a/packages/wasm-utxo/src/lib.rs b/packages/wasm-utxo/src/lib.rs index 3baa8825..c10bfb21 100644 --- a/packages/wasm-utxo/src/lib.rs +++ b/packages/wasm-utxo/src/lib.rs @@ -1,12 +1,8 @@ mod address; -mod descriptor; mod error; -mod fixed_script_wallet; -mod miniscript; +pub mod fixed_script_wallet; mod networks; -mod psbt; -mod try_from_js_value; -mod try_into_js_value; +pub mod wasm; // re-export bitcoin from the miniscript crate // this package is transitioning to a all-purpose bitcoin package, so we want easy access @@ -17,9 +13,7 @@ pub use address::{ to_output_script_with_network, utxolib_compat, }; -pub use descriptor::WrapDescriptor; -pub use miniscript::WrapMiniscript; pub use networks::Network; -pub use psbt::WrapPsbt; +pub use wasm::{WrapDescriptor, WrapMiniscript, WrapPsbt}; pub use crate::fixed_script_wallet::*; diff --git a/packages/wasm-utxo/src/wasm-bindgen.md b/packages/wasm-utxo/src/wasm-bindgen.md index 1cbaa1f2..fb8acf5a 100644 --- a/packages/wasm-utxo/src/wasm-bindgen.md +++ b/packages/wasm-utxo/src/wasm-bindgen.md @@ -8,41 +8,69 @@ Since `wasm-bindgen` flattens all exports to a single module by default, we use ### Rust Side: Namespace Structs +All WASM bindings are located in the `src/wasm/` module, separate from the core implementation. + Create empty structs with `#[wasm_bindgen]` to serve as namespaces, then implement static methods: ```rust -// address/mod.rs +// wasm/address.rs + +use wasm_bindgen::prelude::*; +use wasm_bindgen::JsValue; +use crate::address::networks::{ + to_output_script_with_coin, from_output_script_with_coin_and_format, +}; #[wasm_bindgen] pub struct AddressNamespace; #[wasm_bindgen] impl AddressNamespace { - pub fn to_output_script_with_coin(address: &str, coin: &str) -> Result, WasmError> { - // implementation + pub fn to_output_script_with_coin( + address: &str, + coin: &str, + ) -> Result, JsValue> { + to_output_script_with_coin(address, coin) + .map(|script| script.to_bytes()) + .map_err(|e| JsValue::from_str(&e.to_string())) } pub fn from_output_script_with_coin( script: &[u8], coin: &str, format: Option, - ) -> Result { + ) -> Result { // implementation } } ``` +The namespace structs are exported from `src/wasm/mod.rs`: + +```rust +// wasm/mod.rs + +pub use address::AddressNamespace; +pub use utxolib_compat::UtxolibCompatNamespace; +pub use fixed_script_wallet::FixedScriptWalletNamespace; +// ... other namespace exports +``` + ### Key Conventions -1. **Naming**: Use `*Namespace` suffix for namespace structs (e.g., `AddressNamespace`, `UtxolibCompatNamespace`) +1. **Location**: All WASM bindings are in the `src/wasm/` module, separate from core implementation + +2. **Naming**: Use `*Namespace` suffix for namespace structs (e.g., `AddressNamespace`, `UtxolibCompatNamespace`) -2. **Structure**: Empty structs with no fields - they exist purely for organization +3. **Structure**: Empty structs with no fields - they exist purely for organization -3. **Methods**: All functions are static methods on the namespace struct +4. **Methods**: All functions are static methods on the namespace struct -4. **Case**: Use `snake_case` for method names (Rust convention) - they'll be available as both `snake_case` and `camelCase` in JavaScript +5. **Case**: Use `snake_case` for method names (Rust convention) - they'll be available as both `snake_case` and `camelCase` in JavaScript -5. **Error Handling**: Return `Result` types - `wasm-bindgen` automatically converts these to JavaScript exceptions +6. **Error Handling**: Return `Result` types - `wasm-bindgen` automatically converts these to JavaScript exceptions + +7. **Separation**: WASM binding layer delegates to core implementation in domain modules (e.g., `src/address/`, `src/fixed_script_wallet/`) ### Generated TypeScript @@ -76,15 +104,17 @@ The wrapper layer: ### Example Flow -1. **Rust**: Define `AddressNamespace` struct with static methods -2. **wasm-bindgen**: Generates `AddressNamespace` class in `wasm_utxo.d.ts` -3. **TypeScript Wrapper**: `address.ts` wraps it with precise types -4. **Main Export**: `index.ts` exports it as `export * as address from "./address"` +1. **Core Implementation**: Business logic in domain modules (e.g., `src/address/networks.rs`) +2. **WASM Bindings**: Define `AddressNamespace` struct with static methods in `src/wasm/address.rs` that calls into core implementation +3. **Module Export**: Export namespace from `src/wasm/mod.rs` +4. **wasm-bindgen Generation**: Generates `AddressNamespace` class in `wasm/wasm_utxo.d.ts` +5. **TypeScript Wrapper**: `js/address.ts` wraps it with precise types +6. **Main Export**: `js/index.ts` exports it as `export * as address from "./address"` -This three-layer approach gives us: +This layered approach gives us: -- Clear organization in Rust -- Automatic WASM bindings +- Clear separation between core implementation and WASM bindings +- Automatic WASM bindings generation - Type-safe, well-documented TypeScript API ## Type Mapping @@ -103,14 +133,18 @@ Common Rust ↔ JavaScript type mappings: ## Best Practices -1. **Keep namespace structs empty** - They're purely for organization +1. **Separate bindings from implementation** - Core logic goes in domain modules (`src/address/`, `src/fixed_script_wallet/`), WASM bindings go in `src/wasm/` + +2. **Keep namespace structs empty** - They're purely for organization + +3. **Use descriptive namespace names** - Clear what domain they cover (e.g., `AddressNamespace`, `PsbtNamespace`) -2. **Use descriptive namespace names** - Clear what domain they cover (e.g., `AddressNamespace`, `PsbtNamespace`) +4. **Thin binding layer** - WASM methods should delegate to core implementation, only handling type conversions -3. **Return `Result` types** - Let `wasm-bindgen` handle error conversion to JavaScript exceptions +5. **Return `Result` types** - Let `wasm-bindgen` handle error conversion to JavaScript exceptions -4. **Avoid complex types in signatures** - Stick to primitives and byte arrays when possible; use `JsValue` for complex types +6. **Avoid complex types in signatures** - Stick to primitives and byte arrays when possible; use `JsValue` for complex types -5. **Document with Rust doc comments** - They'll appear in the generated TypeScript +7. **Document with Rust doc comments** - They'll appear in the generated TypeScript -6. **Coordinate with TypeScript wrappers** - Keep the wrapper layer in mind when designing the Rust API +8. **Coordinate with TypeScript wrappers** - Keep the wrapper layer in mind when designing the Rust API diff --git a/packages/wasm-utxo/src/wasm/address.rs b/packages/wasm-utxo/src/wasm/address.rs new file mode 100644 index 00000000..3bc0c133 --- /dev/null +++ b/packages/wasm-utxo/src/wasm/address.rs @@ -0,0 +1,37 @@ +use crate::address::networks::{ + from_output_script_with_coin_and_format, to_output_script_with_coin, AddressFormat, +}; +use miniscript::bitcoin::Script; +use wasm_bindgen::prelude::*; +use wasm_bindgen::JsValue; + +#[wasm_bindgen] +pub struct AddressNamespace; + +#[wasm_bindgen] +impl AddressNamespace { + #[wasm_bindgen] + pub fn to_output_script_with_coin( + address: &str, + coin: &str, + ) -> std::result::Result, JsValue> { + to_output_script_with_coin(address, coin) + .map(|script| script.to_bytes()) + .map_err(|e| JsValue::from_str(&e.to_string())) + } + + #[wasm_bindgen] + pub fn from_output_script_with_coin( + script: &[u8], + coin: &str, + format: Option, + ) -> std::result::Result { + let script_obj = Script::from_bytes(script); + let format_str = format.as_deref(); + let address_format = AddressFormat::from_optional_str(format_str) + .map_err(|e| JsValue::from_str(&e.to_string()))?; + + from_output_script_with_coin_and_format(script_obj, coin, address_format) + .map_err(|e| JsValue::from_str(&e.to_string())) + } +} diff --git a/packages/wasm-utxo/src/fixed_script_wallet/bip32interface.rs b/packages/wasm-utxo/src/wasm/bip32interface.rs similarity index 96% rename from packages/wasm-utxo/src/fixed_script_wallet/bip32interface.rs rename to packages/wasm-utxo/src/wasm/bip32interface.rs index ef217704..73d47fbc 100644 --- a/packages/wasm-utxo/src/fixed_script_wallet/bip32interface.rs +++ b/packages/wasm-utxo/src/wasm/bip32interface.rs @@ -2,7 +2,7 @@ use std::str::FromStr; use crate::bitcoin::bip32::Xpub; use crate::error::WasmUtxoError; -use crate::try_from_js_value::{get_buffer_field, get_field, get_nested_field}; +use crate::wasm::try_from_js_value::{get_buffer_field, get_field, get_nested_field}; use wasm_bindgen::JsValue; fn try_xpub_from_bip32_properties(bip32_key: &JsValue) -> Result { diff --git a/packages/wasm-utxo/src/descriptor.rs b/packages/wasm-utxo/src/wasm/descriptor.rs similarity index 97% rename from packages/wasm-utxo/src/descriptor.rs rename to packages/wasm-utxo/src/wasm/descriptor.rs index 903c12d8..ae876e25 100644 --- a/packages/wasm-utxo/src/descriptor.rs +++ b/packages/wasm-utxo/src/wasm/descriptor.rs @@ -1,5 +1,5 @@ use crate::error::WasmUtxoError; -use crate::try_into_js_value::TryIntoJsValue; +use crate::wasm::try_into_js_value::TryIntoJsValue; use miniscript::bitcoin::secp256k1::{Secp256k1, Signing}; use miniscript::bitcoin::ScriptBuf; use miniscript::descriptor::KeyMap; @@ -137,6 +137,7 @@ impl WrapDescriptor { /// /// # Example /// ``` + /// use wasm_utxo::WrapDescriptor; /// let desc = WrapDescriptor::from_string( /// "pk(xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8/*)", /// "derivable" @@ -167,6 +168,7 @@ impl WrapDescriptor { /// /// # Example /// ``` + /// use wasm_utxo::WrapDescriptor; /// // Will be parsed as definite since it has no wildcards /// let desc = WrapDescriptor::from_string_detect_type( /// "pk(02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5)" @@ -222,7 +224,7 @@ mod tests { assert!(matches!( desc, WrapDescriptor { - 0: crate::descriptor::WrapDescriptorEnum::Definite(_), + 0: crate::wasm::descriptor::WrapDescriptorEnum::Definite(_), } )); } diff --git a/packages/wasm-utxo/src/wasm/fixed_script_wallet.rs b/packages/wasm-utxo/src/wasm/fixed_script_wallet.rs new file mode 100644 index 00000000..2931e314 --- /dev/null +++ b/packages/wasm-utxo/src/wasm/fixed_script_wallet.rs @@ -0,0 +1,66 @@ +use wasm_bindgen::prelude::*; +use wasm_bindgen::JsValue; + +use crate::address::networks::AddressFormat; +use crate::address::utxolib_compat::UtxolibNetwork; +use crate::error::WasmUtxoError; +use crate::fixed_script_wallet::{Chain, WalletScripts}; +use crate::wasm::try_from_js_value::TryFromJsValue; +use crate::wasm::wallet_keys_helpers::root_wallet_keys_from_jsvalue; + +#[wasm_bindgen] +pub struct FixedScriptWalletNamespace; + +#[wasm_bindgen] +impl FixedScriptWalletNamespace { + #[wasm_bindgen] + pub fn output_script( + keys: JsValue, + chain: u32, + index: u32, + network: JsValue, + ) -> Result, WasmUtxoError> { + let network = UtxolibNetwork::try_from_js_value(&network)?; + let chain = Chain::try_from(chain) + .map_err(|e| WasmUtxoError::new(&format!("Invalid chain: {}", e)))?; + + let wallet_keys = root_wallet_keys_from_jsvalue(&keys)?; + let scripts = WalletScripts::from_wallet_keys( + &wallet_keys, + chain, + index, + &network.output_script_support(), + )?; + Ok(scripts.output_script().to_bytes()) + } + + #[wasm_bindgen] + pub fn address( + keys: JsValue, + chain: u32, + index: u32, + network: JsValue, + address_format: Option, + ) -> Result { + let network = UtxolibNetwork::try_from_js_value(&network)?; + let wallet_keys = root_wallet_keys_from_jsvalue(&keys)?; + let chain = Chain::try_from(chain) + .map_err(|e| WasmUtxoError::new(&format!("Invalid chain: {}", e)))?; + let scripts = WalletScripts::from_wallet_keys( + &wallet_keys, + chain, + index, + &network.output_script_support(), + )?; + let script = scripts.output_script(); + let address_format = AddressFormat::from_optional_str(address_format.as_deref()) + .map_err(|e| WasmUtxoError::new(&format!("Invalid address format: {}", e)))?; + let address = crate::address::utxolib_compat::from_output_script_with_network( + &script, + &network, + address_format, + ) + .map_err(|e| WasmUtxoError::new(&format!("Failed to generate address: {}", e)))?; + Ok(address.to_string()) + } +} diff --git a/packages/wasm-utxo/src/miniscript.rs b/packages/wasm-utxo/src/wasm/miniscript.rs similarity index 98% rename from packages/wasm-utxo/src/miniscript.rs rename to packages/wasm-utxo/src/wasm/miniscript.rs index c427a22a..00dd73db 100644 --- a/packages/wasm-utxo/src/miniscript.rs +++ b/packages/wasm-utxo/src/wasm/miniscript.rs @@ -1,5 +1,5 @@ use crate::error::WasmUtxoError; -use crate::try_into_js_value::TryIntoJsValue; +use crate::wasm::try_into_js_value::TryIntoJsValue; use miniscript::bitcoin::{PublicKey, XOnlyPublicKey}; use miniscript::{bitcoin, Legacy, Miniscript, Segwitv0, Tap}; use std::fmt; diff --git a/packages/wasm-utxo/src/wasm/mod.rs b/packages/wasm-utxo/src/wasm/mod.rs new file mode 100644 index 00000000..17a4fbb0 --- /dev/null +++ b/packages/wasm-utxo/src/wasm/mod.rs @@ -0,0 +1,17 @@ +mod address; +mod bip32interface; +mod descriptor; +mod fixed_script_wallet; +mod miniscript; +mod psbt; +mod try_from_js_value; +mod try_into_js_value; +mod utxolib_compat; +pub(crate) mod wallet_keys_helpers; + +pub use address::AddressNamespace; +pub use descriptor::WrapDescriptor; +pub use fixed_script_wallet::FixedScriptWalletNamespace; +pub use miniscript::WrapMiniscript; +pub use psbt::WrapPsbt; +pub use utxolib_compat::UtxolibCompatNamespace; diff --git a/packages/wasm-utxo/src/psbt.rs b/packages/wasm-utxo/src/wasm/psbt.rs similarity index 98% rename from packages/wasm-utxo/src/psbt.rs rename to packages/wasm-utxo/src/wasm/psbt.rs index a44ed9e4..96cdf98c 100644 --- a/packages/wasm-utxo/src/psbt.rs +++ b/packages/wasm-utxo/src/wasm/psbt.rs @@ -1,7 +1,7 @@ -use crate::descriptor::WrapDescriptorEnum; use crate::error::WasmUtxoError; -use crate::try_into_js_value::TryIntoJsValue; -use crate::WrapDescriptor; +use crate::wasm::descriptor::WrapDescriptorEnum; +use crate::wasm::try_into_js_value::TryIntoJsValue; +use crate::wasm::WrapDescriptor; use miniscript::bitcoin::bip32::Fingerprint; use miniscript::bitcoin::secp256k1::{Secp256k1, Signing}; use miniscript::bitcoin::{bip32, psbt, PublicKey, XOnlyPublicKey}; @@ -173,7 +173,7 @@ impl Clone for WrapPsbt { #[cfg(test)] mod tests { - use crate::psbt::SingleKeySigner; + use crate::wasm::psbt::SingleKeySigner; use base64::prelude::*; use miniscript::bitcoin::bip32::{DerivationPath, Fingerprint, KeySource}; use miniscript::bitcoin::psbt::{SigningKeys, SigningKeysMap}; diff --git a/packages/wasm-utxo/src/try_from_js_value.rs b/packages/wasm-utxo/src/wasm/try_from_js_value.rs similarity index 100% rename from packages/wasm-utxo/src/try_from_js_value.rs rename to packages/wasm-utxo/src/wasm/try_from_js_value.rs diff --git a/packages/wasm-utxo/src/try_into_js_value.rs b/packages/wasm-utxo/src/wasm/try_into_js_value.rs similarity index 100% rename from packages/wasm-utxo/src/try_into_js_value.rs rename to packages/wasm-utxo/src/wasm/try_into_js_value.rs diff --git a/packages/wasm-utxo/src/wasm/utxolib_compat.rs b/packages/wasm-utxo/src/wasm/utxolib_compat.rs new file mode 100644 index 00000000..7500aa01 --- /dev/null +++ b/packages/wasm-utxo/src/wasm/utxolib_compat.rs @@ -0,0 +1,66 @@ +use crate::address::networks::AddressFormat; +use crate::address::utxolib_compat::{ + from_output_script_with_network, to_output_script_with_network, UtxolibNetwork, +}; +use crate::wasm::try_from_js_value::TryFromJsValue; +use miniscript::bitcoin::Script; +use wasm_bindgen::prelude::*; +use wasm_bindgen::JsValue; + +#[wasm_bindgen] +pub struct UtxolibCompatNamespace; + +#[wasm_bindgen] +impl UtxolibCompatNamespace { + /// Convert output script to address string + /// + /// # Arguments + /// * `script` - The output script as a byte array + /// * `network` - The UtxolibNetwork object from JavaScript + /// * `format` - Optional address format: "default" or "cashaddr" (only applicable for Bitcoin Cash and eCash) + #[wasm_bindgen] + pub fn from_output_script( + script: &[u8], + network: JsValue, + format: Option, + ) -> std::result::Result { + let network = UtxolibNetwork::try_from_js_value(&network) + .map_err(|e| JsValue::from_str(&e.to_string()))?; + + let script_obj = Script::from_bytes(script); + + let format_str = format.as_deref(); + let address_format = AddressFormat::from_optional_str(format_str) + .map_err(|e| JsValue::from_str(&e.to_string()))?; + + from_output_script_with_network(script_obj, &network, address_format) + .map_err(|e| JsValue::from_str(&e.to_string())) + } + + /// Convert address string to output script + /// + /// # Arguments + /// * `address` - The address string + /// * `network` - The UtxolibNetwork object from JavaScript + /// * `format` - Optional address format (currently unused for decoding as all formats are accepted) + #[wasm_bindgen] + pub fn to_output_script( + address: &str, + network: JsValue, + format: Option, + ) -> std::result::Result, JsValue> { + let network = UtxolibNetwork::try_from_js_value(&network) + .map_err(|e| JsValue::from_str(&e.to_string()))?; + + // Validate format parameter even though we don't use it for decoding + if let Some(fmt) = format { + let format_str = Some(fmt.as_str()); + AddressFormat::from_optional_str(format_str) + .map_err(|e| JsValue::from_str(&e.to_string()))?; + } + + to_output_script_with_network(address, &network) + .map(|script| script.to_bytes()) + .map_err(|e| JsValue::from_str(&e.to_string())) + } +} diff --git a/packages/wasm-utxo/src/wasm/wallet_keys_helpers.rs b/packages/wasm-utxo/src/wasm/wallet_keys_helpers.rs new file mode 100644 index 00000000..20f439a0 --- /dev/null +++ b/packages/wasm-utxo/src/wasm/wallet_keys_helpers.rs @@ -0,0 +1,137 @@ +use std::convert::TryInto; +use std::str::FromStr; + +use crate::bitcoin::bip32::DerivationPath; +use crate::error::WasmUtxoError; +use crate::fixed_script_wallet::{xpub_triple_from_strings, RootWalletKeys, XpubTriple}; +use crate::wasm::bip32interface::xpub_from_bip32interface; +use wasm_bindgen::JsValue; + +pub fn xpub_triple_from_jsvalue(keys: &JsValue) -> Result { + let keys_array = js_sys::Array::from(keys); + if keys_array.length() != 3 { + return Err(WasmUtxoError::new("Expected exactly 3 xpub keys")); + } + + let key_strings: Result<[String; 3], _> = (0..3) + .map(|i| { + keys_array + .get(i) + .as_string() + .ok_or_else(|| WasmUtxoError::new(&format!("Key at index {} is not a string", i))) + }) + .collect::, _>>() + .and_then(|v| { + v.try_into() + .map_err(|_| WasmUtxoError::new("Failed to convert to array")) + }); + + xpub_triple_from_strings(&key_strings?) +} + +pub fn root_wallet_keys_from_jsvalue(keys: &JsValue) -> Result { + // Check if keys is an array (xpub strings) or an object (WalletKeys/RootWalletKeys) + if js_sys::Array::is_array(keys) { + // Handle array of xpub strings + let xpubs = xpub_triple_from_jsvalue(keys)?; + Ok(RootWalletKeys::new_with_derivation_prefixes( + xpubs, + [ + DerivationPath::from_str("m/0/0").unwrap(), + DerivationPath::from_str("m/0/0").unwrap(), + DerivationPath::from_str("m/0/0").unwrap(), + ], + )) + } else if keys.is_object() { + // Handle WalletKeys/RootWalletKeys object + let obj = js_sys::Object::from(keys.clone()); + + // Get the triple property + let triple = js_sys::Reflect::get(&obj, &JsValue::from_str("triple")) + .map_err(|_| WasmUtxoError::new("Failed to get 'triple' property"))?; + + if !js_sys::Array::is_array(&triple) { + return Err(WasmUtxoError::new("'triple' property must be an array")); + } + + let triple_array = js_sys::Array::from(&triple); + if triple_array.length() != 3 { + return Err(WasmUtxoError::new("'triple' must contain exactly 3 keys")); + } + + // Extract xpubs from BIP32Interface objects + let xpubs: XpubTriple = (0..3) + .map(|i| { + let bip32_key = triple_array.get(i); + xpub_from_bip32interface(&bip32_key) + }) + .collect::, _>>()? + .try_into() + .map_err(|_| WasmUtxoError::new("Failed to convert to array"))?; + + // Try to get derivationPrefixes if present (for RootWalletKeys) + let derivation_prefixes = + js_sys::Reflect::get(&obj, &JsValue::from_str("derivationPrefixes")) + .ok() + .and_then(|prefixes| { + if prefixes.is_undefined() || prefixes.is_null() { + return None; + } + + if !js_sys::Array::is_array(&prefixes) { + return None; + } + + let prefixes_array = js_sys::Array::from(&prefixes); + if prefixes_array.length() != 3 { + return None; + } + + let prefix_strings: Result<[String; 3], _> = (0..3) + .map(|i| { + prefixes_array + .get(i) + .as_string() + .ok_or_else(|| WasmUtxoError::new("Prefix is not a string")) + }) + .collect::, _>>() + .and_then(|v| { + v.try_into() + .map_err(|_| WasmUtxoError::new("Failed to convert to array")) + }); + + prefix_strings.ok() + }); + + // Convert prefix strings to DerivationPath + let derivation_paths = if let Some(prefixes) = derivation_prefixes { + prefixes + .iter() + .map(|p| { + // Remove leading 'm/' if present and add it back + let p = p.strip_prefix("m/").unwrap_or(p); + DerivationPath::from_str(&format!("m/{}", p)).map_err(|e| { + WasmUtxoError::new(&format!("Invalid derivation prefix: {}", e)) + }) + }) + .collect::, _>>()? + .try_into() + .map_err(|_| WasmUtxoError::new("Failed to convert derivation paths"))? + } else { + [ + DerivationPath::from_str("m/0/0").unwrap(), + DerivationPath::from_str("m/0/0").unwrap(), + DerivationPath::from_str("m/0/0").unwrap(), + ] + }; + + Ok(RootWalletKeys::new_with_derivation_prefixes( + xpubs, + derivation_paths, + )) + } else { + Err(WasmUtxoError::new( + "Expected array of xpub strings or WalletKeys object", + )) + } +} From 7270c9529f4f572a23f8d5b1268c5cb4568a93ea Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Thu, 30 Oct 2025 11:31:17 +0100 Subject: [PATCH 2/2] feat(wasm-utxo): add CLI tool for address operations and PSBT parsing Add a new CLI tool to provide easy access to wasm-utxo functionality from the command line. Initial implementation supports: - Address encoding/decoding across multiple networks - PSBT parsing with tree-based visualization Modified Cargo setup to use workspace and expose library as rlib. Issue: BTC-2652 Co-authored-by: llm-git --- packages/wasm-utxo/Cargo.lock | 1066 ++++++++++++++++- packages/wasm-utxo/Cargo.toml | 6 +- packages/wasm-utxo/README.md | 1 + packages/wasm-utxo/cli/Cargo.lock | 551 +++++++++ packages/wasm-utxo/cli/Cargo.toml | 21 + packages/wasm-utxo/cli/README.md | 146 +++ packages/wasm-utxo/cli/src/address.rs | 75 ++ packages/wasm-utxo/cli/src/format/fixtures.rs | 118 ++ packages/wasm-utxo/cli/src/format/mod.rs | 9 + packages/wasm-utxo/cli/src/format/tests.rs | 114 ++ packages/wasm-utxo/cli/src/format/tree.rs | 214 ++++ packages/wasm-utxo/cli/src/main.rs | 40 + packages/wasm-utxo/cli/src/node.rs | 345 ++++++ packages/wasm-utxo/cli/src/parse_node.rs | 319 +++++ packages/wasm-utxo/cli/src/psbt.rs | 76 ++ .../cli/tests/fixtures/complex_tree.txt | 13 + .../cli/tests/fixtures/large_buffer.txt | 1 + .../cli/tests/fixtures/numeric_types.txt | 14 + .../cli/tests/fixtures/simple_tree.txt | 4 + .../cli/tests/fixtures/small_buffer.txt | 1 + 20 files changed, 3120 insertions(+), 14 deletions(-) create mode 100644 packages/wasm-utxo/cli/Cargo.lock create mode 100644 packages/wasm-utxo/cli/Cargo.toml create mode 100644 packages/wasm-utxo/cli/README.md create mode 100644 packages/wasm-utxo/cli/src/address.rs create mode 100644 packages/wasm-utxo/cli/src/format/fixtures.rs create mode 100644 packages/wasm-utxo/cli/src/format/mod.rs create mode 100644 packages/wasm-utxo/cli/src/format/tests.rs create mode 100644 packages/wasm-utxo/cli/src/format/tree.rs create mode 100644 packages/wasm-utxo/cli/src/main.rs create mode 100644 packages/wasm-utxo/cli/src/node.rs create mode 100644 packages/wasm-utxo/cli/src/parse_node.rs create mode 100644 packages/wasm-utxo/cli/src/psbt.rs create mode 100644 packages/wasm-utxo/cli/tests/fixtures/complex_tree.txt create mode 100644 packages/wasm-utxo/cli/tests/fixtures/large_buffer.txt create mode 100644 packages/wasm-utxo/cli/tests/fixtures/numeric_types.txt create mode 100644 packages/wasm-utxo/cli/tests/fixtures/simple_tree.txt create mode 100644 packages/wasm-utxo/cli/tests/fixtures/small_buffer.txt diff --git a/packages/wasm-utxo/Cargo.lock b/packages/wasm-utxo/Cargo.lock index bba3fd87..b77c8851 100644 --- a/packages/wasm-utxo/Cargo.lock +++ b/packages/wasm-utxo/Cargo.lock @@ -2,12 +2,109 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +dependencies = [ + "windows-sys 0.60.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.60.2", +] + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "arraydeque" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" + [[package]] name = "arrayvec" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + [[package]] name = "base58ck" version = "0.1.0" @@ -18,6 +115,12 @@ dependencies = [ "bitcoin_hashes", ] +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + [[package]] name = "base64" version = "0.22.1" @@ -78,6 +181,24 @@ dependencies = [ "hex-conservative", ] +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +dependencies = [ + "serde_core", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "bumpalo" version = "3.16.0" @@ -99,6 +220,248 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "clap" +version = "4.5.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "colored" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" +dependencies = [ + "lazy_static 1.5.0", + "windows-sys 0.59.0", +] + +[[package]] +name = "config" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68578f196d2a33ff61b27fae256c3164f65e36382648e30666dde05b8cc9dfdf" +dependencies = [ + "async-trait", + "convert_case", + "json5", + "nom", + "pathdiff", + "ron", + "rust-ini", + "serde", + "serde_json", + "toml", + "yaml-rust2", +] + +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom", + "once_cell", + "tiny-keccak", +] + +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "directories" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + +[[package]] +name = "dlv-list" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" +dependencies = [ + "const-random", +] + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + +[[package]] +name = "generic-array" +version = "0.14.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] + +[[package]] +name = "hashbrown" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" + +[[package]] +name = "hashlink" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" +dependencies = [ + "hashbrown 0.14.5", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hex" version = "0.4.3" @@ -120,6 +483,22 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd" +[[package]] +name = "indexmap" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" +dependencies = [ + "equivalent", + "hashbrown 0.16.0", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + [[package]] name = "itoa" version = "1.0.15" @@ -136,6 +515,45 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "json5" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" +dependencies = [ + "pest", + "pest_derive", + "serde", +] + +[[package]] +name = "lazy_static" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76f033c7ad61445c5b347c7382dd1237847eb1bce590fe50365dcb33d546be73" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.177" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" + +[[package]] +name = "libredox" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" +dependencies = [ + "bitflags", + "libc", +] + [[package]] name = "log" version = "0.4.22" @@ -158,20 +576,163 @@ dependencies = [ "walkdir", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniscript" version = "12.3.4" source = "git+https://github.com/BitGo/rust-miniscript?tag=miniscript-12.3.4-opdrop#4e730b3a1ccc0116e6f6e05ddbf132938cc8395c" dependencies = [ - "bech32", - "bitcoin", + "bech32", + "bitcoin", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82881c4be219ab5faaf2ad5e5e5ecdff8c66bd7402ca3160975c93b24961afd1" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "ordered-float" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" +dependencies = [ + "num-traits", +] + +[[package]] +name = "ordered-multimap" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" +dependencies = [ + "dlv-list", + "hashbrown 0.14.5", +] + +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + +[[package]] +name = "pest" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "989e7521a040efde50c3ab6bbadafbe15ab6dc042686926be59ac35d74607df4" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "187da9a3030dbafabbbfb20cb323b976dc7b7ce91fcd84f2f74d6e31d378e2de" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49b401d98f5757ebe97a26085998d6c0eecec4995cad6ab7fc30ffdf4b052843" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f27a2cfee9f9039c4d86faa5af122a0ac3851441a34865b8a043b46be0065a" +dependencies = [ + "pest", + "sha2", +] + +[[package]] +name = "petgraph" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +dependencies = [ + "fixedbitset", + "indexmap", ] [[package]] -name = "once_cell" -version = "1.20.1" +name = "portable-atomic" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82881c4be219ab5faaf2ad5e5e5ecdff8c66bd7402ca3160975c93b24961afd1" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" [[package]] name = "proc-macro2" @@ -182,6 +743,21 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "ptree" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "289cfd20ebec0e7ff2572e370dd7a1c9973ba666d3c38c5e747de0a4ada21f17" +dependencies = [ + "anstyle", + "config", + "directories", + "petgraph", + "serde", + "serde-value", + "tint", +] + [[package]] name = "quote" version = "1.0.37" @@ -191,6 +767,39 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom", + "libredox", + "thiserror", +] + +[[package]] +name = "ron" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" +dependencies = [ + "base64 0.21.7", + "bitflags", + "serde", + "serde_derive", +] + +[[package]] +name = "rust-ini" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e0698206bcb8882bf2a9ecb4c1e7785db57ff052297085a6efd4fe42302068a" +dependencies = [ + "cfg-if", + "ordered-multimap", +] + [[package]] name = "rustversion" version = "1.0.22" @@ -233,18 +842,38 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.210" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-value" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" +dependencies = [ + "ordered-float", + "serde", +] + +[[package]] +name = "serde_core" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.210" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -263,29 +892,164 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "syn" -version = "2.0.79" +version = "2.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590" +checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tint" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7af24570664a3074673dbbf69a65bdae0ae0b72f2949b1adfbacb736ee4d6896" +dependencies = [ + "lazy_static 0.2.11", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "toml_write", + "winnow", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + [[package]] name = "unicode-ident" version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "walkdir" version = "2.5.0" @@ -296,6 +1060,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + [[package]] name = "wasm-bindgen" version = "0.2.104" @@ -396,7 +1166,7 @@ dependencies = [ name = "wasm-utxo" version = "0.1.0" dependencies = [ - "base64", + "base64 0.22.1", "bech32", "hex", "js-sys", @@ -407,6 +1177,23 @@ dependencies = [ "wasm-bindgen-test", ] +[[package]] +name = "wasm-utxo-cli" +version = "0.1.0" +dependencies = [ + "anyhow", + "base64 0.21.7", + "bitcoin", + "clap", + "colored", + "hex", + "num-bigint", + "ptree", + "serde", + "serde_json", + "wasm-utxo", +] + [[package]] name = "web-sys" version = "0.3.81" @@ -423,7 +1210,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -432,6 +1219,33 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -440,3 +1254,229 @@ checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ "windows-link", ] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +dependencies = [ + "memchr", +] + +[[package]] +name = "yaml-rust2" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8902160c4e6f2fb145dbe9d6760a75e3c9522d8bf796ed7047c85919ac7115f8" +dependencies = [ + "arraydeque", + "encoding_rs", + "hashlink", +] + +[[package]] +name = "zerocopy" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/packages/wasm-utxo/Cargo.toml b/packages/wasm-utxo/Cargo.toml index 17b3b145..0be6d45a 100644 --- a/packages/wasm-utxo/Cargo.toml +++ b/packages/wasm-utxo/Cargo.toml @@ -1,10 +1,14 @@ +[workspace] +members = [".", "cli"] +resolver = "2" + [package] name = "wasm-utxo" version = "0.1.0" edition = "2021" [lib] -crate-type = ["cdylib"] +crate-type = ["cdylib", "rlib"] [lints.clippy] all = "warn" diff --git a/packages/wasm-utxo/README.md b/packages/wasm-utxo/README.md index 5c870fca..249196fa 100644 --- a/packages/wasm-utxo/README.md +++ b/packages/wasm-utxo/README.md @@ -9,6 +9,7 @@ that help verify and co-sign transactions built by the BitGo Wallet Platform API - **[`src/wasm-bindgen.md`](src/wasm-bindgen.md)** - Guide for creating WASM bindings using the namespace pattern - **[`js/README.md`](js/README.md)** - TypeScript wrapper layer architecture and best practices +- **[`cli/README.md`](cli/README.md)** - Command-line interface for address and PSBT operations ## Status diff --git a/packages/wasm-utxo/cli/Cargo.lock b/packages/wasm-utxo/cli/Cargo.lock new file mode 100644 index 00000000..311ddddb --- /dev/null +++ b/packages/wasm-utxo/cli/Cargo.lock @@ -0,0 +1,551 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "base58ck" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c8d66485a3a2ea485c1913c4572ce0256067a5377ac8c75c4960e1cda98605f" +dependencies = [ + "bitcoin-internals", + "bitcoin_hashes", +] + +[[package]] +name = "bech32" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d965446196e3b7decd44aa7ee49e31d630118f90ef12f97900f262eb915c951d" + +[[package]] +name = "bitcoin" +version = "0.32.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda569d741b895131a88ee5589a467e73e9c4718e958ac9308e4f7dc44b6945" +dependencies = [ + "base58ck", + "bech32", + "bitcoin-internals", + "bitcoin-io", + "bitcoin-units", + "bitcoin_hashes", + "hex-conservative", + "hex_lit", + "secp256k1", +] + +[[package]] +name = "bitcoin-internals" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30bdbe14aa07b06e6cfeffc529a1f099e5fbe249524f8125358604df99a4bed2" + +[[package]] +name = "bitcoin-io" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b47c4ab7a93edb0c7198c5535ed9b52b63095f4e9b45279c6736cec4b856baf" + +[[package]] +name = "bitcoin-units" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5285c8bcaa25876d07f37e3d30c303f2609179716e11d688f51e8f1fe70063e2" +dependencies = [ + "bitcoin-internals", +] + +[[package]] +name = "bitcoin_hashes" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb18c03d0db0247e147a21a6faafd5a7eb851c743db062de72018b6b7e8e4d16" +dependencies = [ + "bitcoin-io", + "hex-conservative", +] + +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "cc" +version = "1.2.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "739eb0f94557554b3ca9a86d2d37bebd49c5e6d0c1d2bda35ba5bdac830befc2" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "clap" +version = "4.5.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "find-msvc-tools" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hex-conservative" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5313b072ce3c597065a808dbf612c4c8e8590bdbf8b579508bf7a762c5eae6cd" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "hex_lit" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "js-sys" +version = "0.3.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "miniscript" +version = "12.3.4" +source = "git+https://github.com/BitGo/rust-miniscript?tag=miniscript-12.3.4-opdrop#4e730b3a1ccc0116e6f6e05ddbf132938cc8395c" +dependencies = [ + "bech32", + "bitcoin", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "proc-macro2" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "secp256k1" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113" +dependencies = [ + "bitcoin_hashes", + "secp256k1-sys", +] + +[[package]] +name = "secp256k1-sys" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4387882333d3aa8cb20530a17c69a3752e97837832f34f6dccc760e715001d9" +dependencies = [ + "cc", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "wasm-bindgen" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-utxo" +version = "0.1.0" +dependencies = [ + "bech32", + "js-sys", + "miniscript", + "wasm-bindgen", +] + +[[package]] +name = "wasm-utxo-cli" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "hex", + "serde_json", + "wasm-utxo", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" diff --git a/packages/wasm-utxo/cli/Cargo.toml b/packages/wasm-utxo/cli/Cargo.toml new file mode 100644 index 00000000..08b6e436 --- /dev/null +++ b/packages/wasm-utxo/cli/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "wasm-utxo-cli" +version = "0.1.0" +edition = "2021" + +[[bin]] +name = "wasm-utxo-cli" +path = "src/main.rs" + +[dependencies] +wasm-utxo = { path = ".." } +clap = { version = "4.5", features = ["derive"] } +anyhow = "1.0" +hex = "0.4" +base64 = "0.21" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +num-bigint = "0.4" +bitcoin = "0.32" +colored = "2.1" +ptree = "0.5" diff --git a/packages/wasm-utxo/cli/README.md b/packages/wasm-utxo/cli/README.md new file mode 100644 index 00000000..4ed3effa --- /dev/null +++ b/packages/wasm-utxo/cli/README.md @@ -0,0 +1,146 @@ +# wasm-utxo-cli + +A command-line interface for Bitcoin UTXO operations, built on top of the `wasm-utxo` library. + +This CLI provides utilities for address encoding/decoding and PSBT inspection across multiple UTXO-based cryptocurrencies. + +## Installation + +### Building from source + +```bash +cd cli +cargo build --release +``` + +The binary will be available at `target/release/wasm-utxo-cli`. + +### Installing to system + +```bash +cargo install --path . +``` + +## Usage + +### Address Operations + +#### Decode an address to output script (hex) + +```bash +wasm-utxo-cli address decode
[--network ] +``` + +**Examples:** + +```bash +# Decode a Bitcoin P2PKH address +wasm-utxo-cli address decode 1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa +# Output: 76a91462e907b15cbf27d5425399ebf6f0fb50ebb88f1888ac + +# Decode a Bitcoin SegWit address +wasm-utxo-cli address decode bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq +# Output: 0014e8df018c7e326cc253faac7e46cdc51e68542c42 + +# Decode a testnet address +wasm-utxo-cli address decode tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx --network testnet +``` + +#### Encode an output script to an address + +```bash +wasm-utxo-cli address encode [--network ] +``` + +**Examples:** + +```bash +# Encode to Bitcoin address +wasm-utxo-cli address encode 76a91462e907b15cbf27d5425399ebf6f0fb50ebb88f1888ac +# Output: 1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa + +# Encode to Litecoin address +wasm-utxo-cli address encode 76a91462e907b15cbf27d5425399ebf6f0fb50ebb88f1888ac --network litecoin +# Output: LUEweDxDA4WhvWiNXXSxjM9CYzHPJv4QQF + +# Encode SegWit script +wasm-utxo-cli address encode 0014e8df018c7e326cc253faac7e46cdc51e68542c42 +# Output: bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq +``` + +### PSBT Operations + +#### Parse and inspect a PSBT + +```bash +wasm-utxo-cli psbt parse [--no-color] +``` + +The parser supports multiple input formats: + +- Raw binary PSBT files +- Base64-encoded PSBT strings +- Hex-encoded PSBT strings + +**Examples:** + +```bash +# Parse a PSBT from file +wasm-utxo-cli psbt parse transaction.psbt + +# Parse from stdin (useful for piping) +echo "cHNidP8BA..." | wasm-utxo-cli psbt parse - + +# Parse without color output +wasm-utxo-cli psbt parse transaction.psbt --no-color +``` + +The output displays a hierarchical tree view of the PSBT structure, including: + +- Global fields (version, transaction, extended public keys) +- Per-input fields (UTXOs, signatures, scripts, derivation paths) +- Per-output fields (scripts, derivation paths) +- Decoded transaction details + +### Supported Networks + +The CLI supports the following networks (use with `--network` flag): + +- **Bitcoin**: `bitcoin`, `btc` (default) +- **Bitcoin Testnet**: `testnet`, `test`, `testnet3` +- **Bitcoin Testnet4**: `testnet4` +- **Bitcoin Signet**: `signet` +- **Litecoin**: `litecoin`, `ltc` +- **Litecoin Testnet**: `litecointestnet`, `ltctest` +- **Bitcoin Cash**: `bitcoincash`, `bch` +- **Bitcoin Cash Testnet**: `bitcoincashtestnet`, `bchtest` +- **Bitcoin SV**: `bitcoinsv`, `bsv` +- **Bitcoin SV Testnet**: `bitcoinsvtestnet`, `bsvtest` +- **Bitcoin Gold**: `bitcoingold`, `btg` +- **Bitcoin Gold Testnet**: `bitcoingoldtestnet`, `btgtest` +- **Dash**: `dash` +- **Dash Testnet**: `dashtestnet`, `dashtest` +- **Zcash**: `zcash`, `zec` +- **Zcash Testnet**: `zcashtestnet`, `zectest` +- **Dogecoin**: `dogecoin`, `doge` +- **Dogecoin Testnet**: `dogecointestnet`, `dogetest` +- **eCash**: `ecash`, `xec` +- **eCash Testnet**: `ecashtestnet`, `xectest` + +## Development + +### Running tests + +```bash +cargo test +``` + +### Building for production + +```bash +cargo build --release +``` + +## License + +Same license as the parent `wasm-utxo` crate. diff --git a/packages/wasm-utxo/cli/src/address.rs b/packages/wasm-utxo/cli/src/address.rs new file mode 100644 index 00000000..726ce569 --- /dev/null +++ b/packages/wasm-utxo/cli/src/address.rs @@ -0,0 +1,75 @@ +use anyhow::{Context, Result}; +use clap::Subcommand; +use wasm_utxo::bitcoin::Script; +use wasm_utxo::{from_output_script_with_network, to_output_script_with_network, Network}; + +#[derive(Subcommand)] +pub enum AddressCommand { + /// Decode an address to its output script (hex) + Decode { + /// The address to decode + address: String, + /// Network (bitcoin, testnet, litecoin, zcash, etc.) + #[arg(short, long, default_value = "bitcoin")] + network: String, + }, + /// Encode an output script (hex) to an address + Encode { + /// Output script as hex + script: String, + /// Network (bitcoin, testnet, litecoin, zcash, etc.) + #[arg(short, long, default_value = "bitcoin")] + network: String, + }, +} + +pub fn handle_command(command: AddressCommand) -> Result<()> { + match command { + AddressCommand::Decode { address, network } => { + let network = parse_network(&network)?; + let script = to_output_script_with_network(&address, network) + .context("Failed to decode address")?; + println!("{}", hex::encode(script.as_bytes())); + Ok(()) + } + AddressCommand::Encode { script, network } => { + let network = parse_network(&network)?; + let script_bytes = + hex::decode(&script).context("Invalid hex string for output script")?; + let script_obj = Script::from_bytes(&script_bytes); + let address = from_output_script_with_network(script_obj, network) + .context("Failed to encode output script to address")?; + println!("{}", address); + Ok(()) + } + } +} + +fn parse_network(network: &str) -> Result { + // Try utxolib name first (e.g., "bitcoin", "testnet", "bitcoincash") + if let Some(net) = Network::from_utxolib_name(network) { + return Ok(net); + } + + // Try coin name (e.g., "btc", "ltc", "bch") + if let Some(net) = Network::from_coin_name(network) { + return Ok(net); + } + + // Try common aliases + let normalized = network.to_lowercase(); + match normalized.as_str() { + "test" | "testnet3" => Ok(Network::BitcoinTestnet3), + "signet" => Ok(Network::BitcoinPublicSignet), + "ltctest" => Ok(Network::LitecoinTestnet), + "bchtest" => Ok(Network::BitcoinCashTestnet), + "bsvtest" => Ok(Network::BitcoinSVTestnet), + "btgtest" => Ok(Network::BitcoinGoldTestnet), + "dashtest" => Ok(Network::DashTestnet), + "zectest" => Ok(Network::ZcashTestnet), + "dogetest" => Ok(Network::DogecoinTestnet), + "xec" => Ok(Network::Ecash), + "xectest" => Ok(Network::EcashTestnet), + _ => anyhow::bail!("Unknown network: {}", network), + } +} diff --git a/packages/wasm-utxo/cli/src/format/fixtures.rs b/packages/wasm-utxo/cli/src/format/fixtures.rs new file mode 100644 index 00000000..034073c7 --- /dev/null +++ b/packages/wasm-utxo/cli/src/format/fixtures.rs @@ -0,0 +1,118 @@ +use super::tree::{node_to_string_with_scheme, ColorScheme}; +use crate::node::Node; +use std::env; +use std::fs; +use std::io::{self, Write}; +use std::path::PathBuf; + +/// Generate a tree representation of a Node without colors +pub fn generate_tree_text(node: &Node) -> Result { + // Use the no-color scheme for consistent fixture output + let no_color_scheme = ColorScheme::no_color(); + node_to_string_with_scheme(node, &no_color_scheme) +} + +/// Generate a tree representation of a Node with a specific color scheme +pub fn generate_tree_text_with_scheme( + node: &Node, + color_scheme: &ColorScheme, +) -> Result { + node_to_string_with_scheme(node, color_scheme) +} + +/// Returns the path to the fixture directory +pub fn fixtures_directory() -> PathBuf { + let project_dir = env::current_dir().expect("Failed to get current directory"); + project_dir.join("tests").join("fixtures") +} + +/// Write tree output to a fixture file +pub fn write_fixture(name: &str, content: &str) -> Result<(), io::Error> { + let fixtures_dir = fixtures_directory(); + fs::create_dir_all(&fixtures_dir)?; + + let fixture_path = fixtures_dir.join(format!("{}.txt", name)); + + // Write the content to the file + let mut file = fs::File::create(&fixture_path)?; + file.write_all(content.as_bytes())?; + + Ok(()) +} + +/// Read the content of a fixture file if it exists +pub fn read_fixture(name: &str) -> Result, io::Error> { + let fixture_path = fixtures_directory().join(format!("{}.txt", name)); + + if fixture_path.exists() { + let content = fs::read_to_string(&fixture_path)?; + Ok(Some(content)) + } else { + Ok(None) + } +} + +/// Ensure the generated tree output matches the fixture file +/// If the fixture doesn't exist, it will be created +pub fn assert_tree_matches_fixture(node: &Node, name: &str) -> Result<(), io::Error> { + let generated = generate_tree_text(node)?; + + match read_fixture(name)? { + Some(fixture_content) => { + // Compare the generated output to the fixture + assert_eq!( + generated, fixture_content, + "Generated tree output doesn't match fixture file: {}", + name + ); + } + None => { + // Create the fixture if it doesn't exist + write_fixture(name, &generated)?; + println!("Created new fixture: {}.txt", name); + } + } + + Ok(()) +} + +/// Force update of a fixture file with new content +pub fn update_fixture(node: &Node, name: &str) -> Result<(), io::Error> { + let generated = generate_tree_text(node)?; + write_fixture(name, &generated) +} + +// Environment variable to force fixture updates +const UPDATE_FIXTURES_ENV: &str = "UPDATE_FIXTURES"; + +/// Check if fixtures should be updated +pub fn should_update_fixtures() -> bool { + env::var(UPDATE_FIXTURES_ENV).is_ok() +} + +/// Assert tree matches fixture, updating if needed or requested +pub fn assert_or_update_fixture(node: &Node, name: &str) -> Result<(), io::Error> { + let generated = generate_tree_text(node)?; + + match read_fixture(name)? { + Some(fixture_content) => { + if should_update_fixtures() || generated != fixture_content { + write_fixture(name, &generated)?; + println!("Updated fixture: {}.txt", name); + } else { + assert_eq!( + generated, fixture_content, + "Generated tree output doesn't match fixture file: {}", + name + ); + } + } + None => { + // Create the fixture if it doesn't exist + write_fixture(name, &generated)?; + println!("Created new fixture: {}.txt", name); + } + } + + Ok(()) +} diff --git a/packages/wasm-utxo/cli/src/format/mod.rs b/packages/wasm-utxo/cli/src/format/mod.rs new file mode 100644 index 00000000..2c11eee2 --- /dev/null +++ b/packages/wasm-utxo/cli/src/format/mod.rs @@ -0,0 +1,9 @@ +pub mod fixtures; +#[cfg(test)] +mod tests; +mod tree; + +pub use tree::{ + add_node_to_tree, add_node_to_tree_with_scheme, format_primitive_for_tree, node_to_string, + node_to_string_with_scheme, render_tree, render_tree_with_scheme, ColorScheme, +}; diff --git a/packages/wasm-utxo/cli/src/format/tests.rs b/packages/wasm-utxo/cli/src/format/tests.rs new file mode 100644 index 00000000..03370dc3 --- /dev/null +++ b/packages/wasm-utxo/cli/src/format/tests.rs @@ -0,0 +1,114 @@ +#[cfg(test)] +mod tests { + use crate::format::fixtures::assert_or_update_fixture; + use crate::node::{Node, Primitive}; + use num_bigint::BigInt; + + #[test] + fn test_simple_tree() -> std::io::Result<()> { + // Create a simple tree + let child1 = Node::new("name", Primitive::String("Alice".to_string())); + let child2 = Node::new("age", Primitive::U8(30)); + let child3 = Node::new("active", Primitive::Boolean(true)); + + let mut parent = Node::new("person", Primitive::None); + parent.add_child(child1); + parent.add_child(child2); + parent.add_child(child3); + + // Check against fixture + assert_or_update_fixture(&parent, "simple_tree")?; + Ok(()) + } + + #[test] + fn test_complex_tree() -> std::io::Result<()> { + // Create a more complex tree + let address_street = Node::new("street", Primitive::String("123 Main St".to_string())); + let address_city = Node::new("city", Primitive::String("Anytown".to_string())); + let address_zip = Node::new("zip", Primitive::U16(12345)); + + let mut address = Node::new("address", Primitive::None); + address.add_child(address_street); + address.add_child(address_city); + address.add_child(address_zip); + + let phone1 = Node::new("home", Primitive::String("555-1234".to_string())); + let phone2 = Node::new("work", Primitive::String("555-5678".to_string())); + + let mut phones = Node::new("phones", Primitive::None); + phones.add_child(phone1); + phones.add_child(phone2); + + let account_number = Node::new( + "number", + Primitive::Integer(BigInt::parse_bytes(b"9876543210123456", 10).unwrap()), + ); + let account_balance = Node::new("balance", Primitive::I32(5000)); + + let mut account = Node::new("account", Primitive::None); + account.add_child(account_number); + account.add_child(account_balance); + + let name = Node::new("name", Primitive::String("John Doe".to_string())); + let age = Node::new("age", Primitive::U8(35)); + + let mut person = Node::new("person", Primitive::None); + person.add_child(name); + person.add_child(age); + person.add_child(address); + person.add_child(phones); + person.add_child(account); + + // Check against fixture + assert_or_update_fixture(&person, "complex_tree")?; + Ok(()) + } + + #[test] + fn test_buffer_display() -> std::io::Result<()> { + // Test how binary data is formatted in the tree + let small_buffer = Node::new("small", Primitive::Buffer(vec![1, 2, 3, 4])); + assert_or_update_fixture(&small_buffer, "small_buffer")?; + + let large_buffer = Node::new("large", Primitive::Buffer((0..100).collect())); + assert_or_update_fixture(&large_buffer, "large_buffer")?; + + Ok(()) + } + + #[test] + fn test_numeric_types() -> std::io::Result<()> { + // Create a tree with all the numeric types + let mut numbers = Node::new("numbers", Primitive::None); + + // Add signed integers + numbers.add_child(Node::new("i8_min", Primitive::I8(i8::MIN))); + numbers.add_child(Node::new("i8_max", Primitive::I8(i8::MAX))); + numbers.add_child(Node::new("i16_min", Primitive::I16(i16::MIN))); + numbers.add_child(Node::new("i16_max", Primitive::I16(i16::MAX))); + numbers.add_child(Node::new("i32_min", Primitive::I32(i32::MIN))); + numbers.add_child(Node::new("i32_max", Primitive::I32(i32::MAX))); + numbers.add_child(Node::new("i64_min", Primitive::I64(i64::MIN))); + numbers.add_child(Node::new("i64_max", Primitive::I64(i64::MAX))); + + // Add unsigned integers + numbers.add_child(Node::new("u8_max", Primitive::U8(u8::MAX))); + numbers.add_child(Node::new("u16_max", Primitive::U16(u16::MAX))); + numbers.add_child(Node::new("u32_max", Primitive::U32(u32::MAX))); + numbers.add_child(Node::new("u64_max", Primitive::U64(u64::MAX))); + + // Add a big integer + numbers.add_child(Node::new( + "bigint", + Primitive::Integer( + BigInt::parse_bytes(b"12345678901234567890123456789012345678901234567890", 10) + .unwrap(), + ), + )); + + // Check against fixture + assert_or_update_fixture(&numbers, "numeric_types")?; + Ok(()) + } +} diff --git a/packages/wasm-utxo/cli/src/format/tree.rs b/packages/wasm-utxo/cli/src/format/tree.rs new file mode 100644 index 00000000..b09bf1c0 --- /dev/null +++ b/packages/wasm-utxo/cli/src/format/tree.rs @@ -0,0 +1,214 @@ +use crate::node::{Node, Primitive}; +use colored::*; +use ptree::{print_tree, TreeBuilder}; +use std::borrow::Cow; +use std::io; + +/// Defines how different parts of the tree should be styled +#[derive(Clone, Debug)] +pub struct ColorScheme { + /// Style for node labels (field names) + pub label_style: fn(&str) -> String, + /// Style for node values + pub value_style: fn(&str) -> String, + /// Style for buffer values specifically + pub buffer_style: fn(&str) -> String, + /// Style for numeric values specifically + pub numeric_style: fn(&str) -> String, + /// Style for string values specifically + pub string_style: fn(&str) -> String, + /// Style for boolean values specifically + pub boolean_style: fn(&str) -> String, +} + +impl ColorScheme { + /// Default color scheme with bold labels and colored values + pub fn default() -> Self { + Self { + label_style: |s| s.bold().to_string(), + value_style: |s| s.to_string(), + buffer_style: |s| s.cyan().to_string(), + numeric_style: |s| s.yellow().to_string(), + string_style: |s| s.green().to_string(), + boolean_style: |s| s.magenta().to_string(), + } + } + + /// No color scheme - plain text output + pub fn no_color() -> Self { + Self { + label_style: |s| s.to_string(), + value_style: |s| s.to_string(), + buffer_style: |s| s.to_string(), + numeric_style: |s| s.to_string(), + string_style: |s| s.to_string(), + boolean_style: |s| s.to_string(), + } + } + + /// High contrast color scheme for better visibility + pub fn high_contrast() -> Self { + Self { + label_style: |s| s.bold().bright_white().to_string(), + value_style: |s| s.to_string(), + buffer_style: |s| s.bright_cyan().to_string(), + numeric_style: |s| s.bright_yellow().to_string(), + string_style: |s| s.bright_green().to_string(), + boolean_style: |s| s.bright_magenta().to_string(), + } + } + + /// Minimal color scheme with subtle styling + pub fn minimal() -> Self { + Self { + label_style: |s| s.dimmed().to_string(), + value_style: |s| s.to_string(), + buffer_style: |s| s.blue().to_string(), + numeric_style: |s| s.to_string(), + string_style: |s| s.to_string(), + boolean_style: |s| s.to_string(), + } + } + + /// Apply appropriate styling to a primitive value based on its type + pub fn style_primitive(&self, primitive: &Primitive, formatted_value: &str) -> String { + match primitive { + Primitive::Buffer(_) => (self.buffer_style)(formatted_value), + Primitive::U8(_) + | Primitive::U16(_) + | Primitive::U32(_) + | Primitive::U64(_) + | Primitive::I8(_) + | Primitive::I16(_) + | Primitive::I32(_) + | Primitive::I64(_) + | Primitive::Integer(_) => (self.numeric_style)(formatted_value), + Primitive::String(_) => (self.string_style)(formatted_value), + Primitive::Boolean(_) => (self.boolean_style)(formatted_value), + Primitive::None => (self.value_style)(formatted_value), + } + } +} + +/// A wrapper to implement TreeItem for Node +#[derive(Clone)] +struct NodeTreeItem<'a> { + node: &'a Node, + color_scheme: &'a ColorScheme, +} + +impl<'a> ptree::TreeItem for NodeTreeItem<'a> { + type Child = Self; + + fn write_self(&self, f: &mut W, style: &ptree::Style) -> io::Result<()> { + let styled_label = (self.color_scheme.label_style)(&self.node.label); + if self.node.value.is_empty() { + return write!(f, "{}", style.paint(styled_label)); + } + let value_str = format_primitive_for_tree(&self.node.value); + let styled_value = self + .color_scheme + .style_primitive(&self.node.value, &value_str); + let text = format!("{}: {}", styled_label, styled_value); + write!(f, "{}", style.paint(text)) + } + + fn children(&self) -> Cow<'_, [Self::Child]> { + Cow::Owned( + self.node + .children + .iter() + .map(|child| NodeTreeItem { + node: child, + color_scheme: self.color_scheme, + }) + .collect(), + ) + } +} + +/// Format the value of a primitive for display in a tree +pub fn format_primitive_for_tree(primitive: &Primitive) -> String { + match primitive { + Primitive::Buffer(b) => { + // Convert bytes to hex string + let hex = b.iter().map(|b| format!("{:02x}", b)).collect::(); + + // For long buffers, truncate with ellipsis and show length + let hex_trimmed = if hex.len() <= 512 { + hex + } else { + format!("{}...", &hex[0..512]) + }; + + format!("{} ({} bytes)", hex_trimmed, b.len()) + } + // Use to_string() for all other types (numbers, booleans, None) + _ => primitive.to_string(), + } +} + +/// Render a Node tree to a string with the specified color scheme +pub fn node_to_string_with_scheme( + node: &Node, + color_scheme: &ColorScheme, +) -> Result { + let styled_label = (color_scheme.label_style)(&node.label); + let value_str = format_primitive_for_tree(&node.value); + let styled_value = color_scheme.style_primitive(&node.value, &value_str); + let root_text = format!("{}: {}", styled_label, styled_value); + let mut tree = TreeBuilder::new(root_text); + + // Add children + for child in &node.children { + add_node_to_tree_with_scheme(&mut tree, child, color_scheme); + } + + // Build the tree + let tree = tree.build(); + + // Render to string + let mut output = Vec::new(); + ptree::write_tree(&tree, &mut output)?; + Ok(String::from_utf8_lossy(&output).to_string()) +} + +/// Render a Node tree to a string (using default color scheme for backward compatibility) +pub fn node_to_string(node: &Node) -> Result { + node_to_string_with_scheme(node, &ColorScheme::default()) +} + +/// Helper function to add a node and its children to a tree with color scheme +pub fn add_node_to_tree_with_scheme( + tree: &mut TreeBuilder, + node: &Node, + color_scheme: &ColorScheme, +) { + let styled_label = (color_scheme.label_style)(&node.label); + let value_str = format_primitive_for_tree(&node.value); + let styled_value = color_scheme.style_primitive(&node.value, &value_str); + let node_text = format!("{}: {}", styled_label, styled_value); + tree.begin_child(node_text); + + for child in &node.children { + add_node_to_tree_with_scheme(tree, child, color_scheme); + } + + tree.end_child(); +} + +/// Helper function to add a node and its children to a tree (using default color scheme for backward compatibility) +pub fn add_node_to_tree(tree: &mut TreeBuilder, node: &Node) { + add_node_to_tree_with_scheme(tree, node, &ColorScheme::default()); +} + +/// Render a Node tree to the terminal with the specified color scheme +pub fn render_tree_with_scheme(node: &Node, color_scheme: &ColorScheme) -> Result<(), io::Error> { + let tree_item = NodeTreeItem { node, color_scheme }; + print_tree(&tree_item) +} + +/// Render a Node tree to the terminal (using default color scheme for backward compatibility) +pub fn render_tree(node: &Node) -> Result<(), io::Error> { + render_tree_with_scheme(node, &ColorScheme::default()) +} diff --git a/packages/wasm-utxo/cli/src/main.rs b/packages/wasm-utxo/cli/src/main.rs new file mode 100644 index 00000000..aaa7314e --- /dev/null +++ b/packages/wasm-utxo/cli/src/main.rs @@ -0,0 +1,40 @@ +use anyhow::Result; +use clap::{Parser, Subcommand}; + +mod address; +mod format; +mod node; +mod parse_node; +mod psbt; + +#[derive(Parser)] +#[command(name = "wasm-utxo-cli")] +#[command(about = "CLI tool for Bitcoin UTXO operations", long_about = None)] +#[command(version)] +struct Cli { + #[command(subcommand)] + command: Commands, +} + +#[derive(Subcommand)] +enum Commands { + /// Address encoding and decoding operations + Address { + #[command(subcommand)] + command: address::AddressCommand, + }, + /// PSBT parsing and inspection operations + Psbt { + #[command(subcommand)] + command: psbt::PsbtCommand, + }, +} + +fn main() -> Result<()> { + let cli = Cli::parse(); + + match cli.command { + Commands::Address { command } => address::handle_command(command), + Commands::Psbt { command } => psbt::handle_command(command), + } +} diff --git a/packages/wasm-utxo/cli/src/node.rs b/packages/wasm-utxo/cli/src/node.rs new file mode 100644 index 00000000..34e50627 --- /dev/null +++ b/packages/wasm-utxo/cli/src/node.rs @@ -0,0 +1,345 @@ +use num_bigint::BigInt; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use std::fmt; + +pub type Buffer = Vec; + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "type", content = "value")] +pub enum Primitive { + String(String), + #[serde( + serialize_with = "serialize_buffer", + deserialize_with = "deserialize_buffer" + )] + Buffer(Buffer), + #[serde( + serialize_with = "serialize_bigint", + deserialize_with = "deserialize_bigint" + )] + Integer(BigInt), + I8(i8), + I16(i16), + I32(i32), + I64(i64), + U8(u8), + U16(u16), + U32(u32), + U64(u64), + Boolean(bool), + None, +} + +fn serialize_buffer(buffer: &Buffer, serializer: S) -> Result +where + S: Serializer, +{ + serializer.serialize_str(&hex::encode(buffer)) +} + +fn deserialize_buffer<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let s = String::deserialize(deserializer)?; + hex::decode(&s).map_err(serde::de::Error::custom) +} + +fn serialize_bigint(bigint: &BigInt, serializer: S) -> Result +where + S: Serializer, +{ + serializer.serialize_str(&bigint.to_string()) +} + +fn deserialize_bigint<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let s = String::deserialize(deserializer)?; + BigInt::parse_bytes(s.as_bytes(), 10).ok_or_else(|| serde::de::Error::custom("Invalid BigInt")) +} + +impl Primitive { + pub fn is_empty(&self) -> bool { + matches!(self, Primitive::None) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Node { + pub label: String, + pub value: Primitive, + pub children: Vec, +} + +impl Node { + pub fn new(label: impl Into, value: Primitive) -> Self { + Self { + label: label.into(), + value, + children: Vec::new(), + } + } + + pub fn with_children(label: impl Into, value: Primitive, children: Vec) -> Self { + Self { + label: label.into(), + value, + children, + } + } + + pub fn add_child(&mut self, child: Node) { + self.children.push(child); + } + + pub fn with_child(mut self, child: Node) -> Self { + self.children.push(child); + self + } + + pub fn extend(&mut self, nodes: impl IntoIterator) { + self.children.extend(nodes); + } + + pub fn child_count(&self) -> usize { + self.children.len() + } +} + +impl fmt::Display for Primitive { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Primitive::String(s) => write!(f, "{}", s), + Primitive::Buffer(b) => write!(f, "{:?}", b), + Primitive::Integer(n) => write!(f, "{}", n), + Primitive::I8(n) => write!(f, "{}i8", n), + Primitive::I16(n) => write!(f, "{}i16", n), + Primitive::I32(n) => write!(f, "{}i32", n), + Primitive::I64(n) => write!(f, "{}i64", n), + Primitive::U8(n) => write!(f, "{}u8", n), + Primitive::U16(n) => write!(f, "{}u16", n), + Primitive::U32(n) => write!(f, "{}u32", n), + Primitive::U64(n) => write!(f, "{}u64", n), + Primitive::Boolean(b) => write!(f, "{}", b), + Primitive::None => write!(f, "None"), + } + } +} + +impl fmt::Display for Node { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}({})", self.label, self.value)?; + if !self.children.is_empty() { + write!(f, " with {} children", self.children.len())?; + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_node_creation() { + let node = Node::new("name", Primitive::String("John".to_string())); + assert_eq!(node.label, "name"); + assert_eq!(node.child_count(), 0); + + let int_value = BigInt::from(30); + let node = Node::new("age", Primitive::Integer(int_value.clone())); + assert_eq!(node.label, "age"); + if let Primitive::Integer(value) = &node.value { + assert_eq!(value, &int_value); + } else { + panic!("Expected Integer value"); + } + + let large_int = + BigInt::parse_bytes(b"12345678901234567890123456789012345678901234567890", 10).unwrap(); + let node = Node::new("big_num", Primitive::Integer(large_int.clone())); + if let Primitive::Integer(value) = &node.value { + assert_eq!(value, &large_int); + } else { + panic!("Expected Integer value"); + } + + let node = Node::new("active", Primitive::Boolean(true)); + assert_eq!(node.label, "active"); + + let buffer = vec![1, 2, 3, 4]; + let node = Node::new("data", Primitive::Buffer(buffer.clone())); + match &node.value { + Primitive::Buffer(b) => assert_eq!(b, &buffer), + _ => panic!("Expected Buffer value"), + } + + let node = Node::new("empty", Primitive::None); + assert_eq!(node.label, "empty"); + } + + #[test] + fn test_node_with_children() { + let child1 = Node::new("child1", Primitive::String("Child 1".to_string())); + let child2 = Node::new("child2", Primitive::Integer(BigInt::from(42))); + + let mut parent = Node::new("parent", Primitive::None); + assert_eq!(parent.child_count(), 0); + + parent.add_child(child1); + assert_eq!(parent.child_count(), 1); + + parent.add_child(child2); + assert_eq!(parent.child_count(), 2); + + assert_eq!(parent.children[0].label, "child1"); + assert_eq!(parent.children[1].label, "child2"); + } + + #[test] + fn test_node_with_children_constructor() { + let child1 = Node::new("child1", Primitive::String("Child 1".to_string())); + let child2 = Node::new("child2", Primitive::Integer(BigInt::from(42))); + + let children = vec![child1, child2]; + let parent = Node::with_children("parent", Primitive::None, children); + + assert_eq!(parent.child_count(), 2); + assert_eq!(parent.children[0].label, "child1"); + assert_eq!(parent.children[1].label, "child2"); + } + + #[test] + fn test_large_integers() { + let large_int = + BigInt::parse_bytes(b"9999999999999999999999999999999999999999999999999999", 10) + .unwrap(); + let node = Node::new("very_large", Primitive::Integer(large_int.clone())); + + if let Primitive::Integer(value) = &node.value { + assert_eq!(value, &large_int); + } else { + panic!("Expected Integer value"); + } + + let display = format!("{}", node.value); + assert_eq!( + display, + "9999999999999999999999999999999999999999999999999999" + ); + } + + #[test] + fn test_integer_variants() { + let i8_node = Node::new("i8_val", Primitive::I8(-42)); + let i16_node = Node::new("i16_val", Primitive::I16(-1000)); + let i32_node = Node::new("i32_val", Primitive::I32(-100000)); + let i64_node = Node::new("i64_val", Primitive::I64(-5000000000)); + + match &i8_node.value { + Primitive::I8(val) => assert_eq!(*val, -42), + _ => panic!("Expected I8 value"), + } + + match &i16_node.value { + Primitive::I16(val) => assert_eq!(*val, -1000), + _ => panic!("Expected I16 value"), + } + + match &i32_node.value { + Primitive::I32(val) => assert_eq!(*val, -100000), + _ => panic!("Expected I32 value"), + } + + match &i64_node.value { + Primitive::I64(val) => assert_eq!(*val, -5000000000), + _ => panic!("Expected I64 value"), + } + + let u8_node = Node::new("u8_val", Primitive::U8(200)); + let u16_node = Node::new("u16_val", Primitive::U16(60000)); + let u32_node = Node::new("u32_val", Primitive::U32(3000000000)); + let u64_node = Node::new("u64_val", Primitive::U64(9000000000000000000)); + + match &u8_node.value { + Primitive::U8(val) => assert_eq!(*val, 200), + _ => panic!("Expected U8 value"), + } + + match &u16_node.value { + Primitive::U16(val) => assert_eq!(*val, 60000), + _ => panic!("Expected U16 value"), + } + + match &u32_node.value { + Primitive::U32(val) => assert_eq!(*val, 3000000000), + _ => panic!("Expected U32 value"), + } + + match &u64_node.value { + Primitive::U64(val) => assert_eq!(*val, 9000000000000000000), + _ => panic!("Expected U64 value"), + } + } + + #[test] + fn test_integer_display() { + assert_eq!(format!("{}", Primitive::I8(-42)), "-42i8"); + assert_eq!(format!("{}", Primitive::I16(-1000)), "-1000i16"); + assert_eq!(format!("{}", Primitive::I32(-100000)), "-100000i32"); + assert_eq!(format!("{}", Primitive::I64(-5000000000)), "-5000000000i64"); + + assert_eq!(format!("{}", Primitive::U8(200)), "200u8"); + assert_eq!(format!("{}", Primitive::U16(60000)), "60000u16"); + assert_eq!(format!("{}", Primitive::U32(3000000000)), "3000000000u32"); + assert_eq!( + format!("{}", Primitive::U64(9000000000000000000)), + "9000000000000000000u64" + ); + } + + #[test] + fn test_serde_serialization() { + let node = Node::new("test", Primitive::String("hello".to_string())); + let json = serde_json::to_string(&node).unwrap(); + let deserialized: Node = serde_json::from_str(&json).unwrap(); + assert_eq!(node.label, deserialized.label); + + let buffer_node = Node::new("buffer", Primitive::Buffer(vec![0x01, 0x02, 0x03])); + let json = serde_json::to_string(&buffer_node).unwrap(); + assert!(json.contains("010203")); + let deserialized: Node = serde_json::from_str(&json).unwrap(); + if let Primitive::Buffer(b) = &deserialized.value { + assert_eq!(b, &vec![0x01, 0x02, 0x03]); + } else { + panic!("Expected Buffer"); + } + + let bigint_node = Node::new("bigint", Primitive::Integer(BigInt::from(12345))); + let json = serde_json::to_string(&bigint_node).unwrap(); + assert!(json.contains("12345")); + let deserialized: Node = serde_json::from_str(&json).unwrap(); + if let Primitive::Integer(i) = &deserialized.value { + assert_eq!(i, &BigInt::from(12345)); + } else { + panic!("Expected Integer"); + } + } + + #[test] + fn test_serde_with_children() { + let child1 = Node::new("child1", Primitive::U32(42)); + let child2 = Node::new("child2", Primitive::Boolean(true)); + let parent = Node::with_children("parent", Primitive::None, vec![child1, child2]); + + let json = serde_json::to_string(&parent).unwrap(); + let deserialized: Node = serde_json::from_str(&json).unwrap(); + + assert_eq!(deserialized.label, "parent"); + assert_eq!(deserialized.children.len(), 2); + assert_eq!(deserialized.children[0].label, "child1"); + assert_eq!(deserialized.children[1].label, "child2"); + } +} diff --git a/packages/wasm-utxo/cli/src/parse_node.rs b/packages/wasm-utxo/cli/src/parse_node.rs new file mode 100644 index 00000000..d8557298 --- /dev/null +++ b/packages/wasm-utxo/cli/src/parse_node.rs @@ -0,0 +1,319 @@ +/// This contains low-level parsing of PSBT into a node structure suitable for display +use bitcoin::consensus::Decodable; +use bitcoin::hashes::Hash; +use bitcoin::psbt::Psbt; +use bitcoin::{Network, ScriptBuf, Transaction}; + +pub use crate::node::{Node, Primitive}; + +fn script_buf_to_node(label: &str, script_buf: &ScriptBuf) -> Node { + let mut node = Node::new(label, Primitive::Buffer(script_buf.to_bytes())); + node.add_child(Node::new( + "asm", + Primitive::String(script_buf.to_asm_string()), + )); + node +} + +fn bip32_derivations_to_nodes( + bip32_derivation: &std::collections::BTreeMap< + bitcoin::secp256k1::PublicKey, + (bitcoin::bip32::Fingerprint, bitcoin::bip32::DerivationPath), + >, +) -> Vec { + bip32_derivation + .iter() + .map(|(pubkey, (fingerprint, path))| { + let mut derivation_node = Node::new("bip32_derivation", Primitive::None); + derivation_node.add_child(Node::new( + "pubkey", + Primitive::Buffer(pubkey.serialize().to_vec()), + )); + derivation_node.add_child(Node::new( + "fingerprint", + Primitive::Buffer(fingerprint.to_bytes().to_vec()), + )); + derivation_node.add_child(Node::new("path", Primitive::String(path.to_string()))); + derivation_node + }) + .collect() +} + +fn proprietary_to_nodes( + proprietary: &std::collections::BTreeMap>, +) -> Vec { + proprietary + .iter() + .map(|(prop_key, v)| { + let mut prop_node = Node::new("key", Primitive::None); + prop_node.add_child(Node::new( + "prefix", + Primitive::String(String::from_utf8_lossy(&prop_key.prefix).to_string()), + )); + prop_node.add_child(Node::new("subtype", Primitive::U8(prop_key.subtype))); + prop_node.add_child(Node::new( + "key_data", + Primitive::Buffer(prop_key.key.to_vec()), + )); + prop_node.add_child(Node::new("value", Primitive::Buffer(v.to_vec()))); + prop_node + }) + .collect() +} + +fn xpubs_to_nodes( + xpubs: &std::collections::BTreeMap< + bitcoin::bip32::Xpub, + (bitcoin::bip32::Fingerprint, bitcoin::bip32::DerivationPath), + >, +) -> Vec { + xpubs + .iter() + .map(|(xpub, (fingerprint, path))| { + let mut xpub_node = Node::new("xpub", Primitive::None); + xpub_node.add_child(Node::new("xpub", Primitive::String(xpub.to_string()))); + xpub_node.add_child(Node::new( + "fingerprint", + Primitive::Buffer(fingerprint.to_bytes().to_vec()), + )); + xpub_node.add_child(Node::new("path", Primitive::String(path.to_string()))); + xpub_node + }) + .collect() +} + +pub fn xpubs_to_node( + xpubs: &std::collections::BTreeMap< + bitcoin::bip32::Xpub, + (bitcoin::bip32::Fingerprint, bitcoin::bip32::DerivationPath), + >, +) -> Node { + let mut xpubs_node = Node::new("xpubs", Primitive::U64(xpubs.len() as u64)); + for node in xpubs_to_nodes(xpubs) { + xpubs_node.add_child(node); + } + xpubs_node +} + +pub fn psbt_to_node(psbt: &Psbt, network: Network) -> Node { + let mut psbt_node = Node::new("psbt", Primitive::None); + + let tx = &psbt.unsigned_tx; + psbt_node.add_child(tx_to_node(tx, network)); + + psbt_node.add_child(xpubs_to_node(&psbt.xpub)); + + if psbt.proprietary.len() > 0 { + let mut proprietary_node = + Node::new("proprietary", Primitive::U64(psbt.proprietary.len() as u64)); + proprietary_node.extend(proprietary_to_nodes(&psbt.proprietary)); + psbt_node.add_child(proprietary_node); + } + + psbt_node.add_child(Node::new("version", Primitive::U32(psbt.version))); + + let mut inputs_node = Node::new("inputs", Primitive::U64(psbt.inputs.len() as u64)); + for (i, input) in psbt.inputs.iter().enumerate() { + let mut input_node = Node::new(format!("input_{}", i), Primitive::None); + + if let Some(utxo) = &input.non_witness_utxo { + input_node.add_child(Node::new( + "non_witness_utxo", + Primitive::Buffer(utxo.compute_txid().to_byte_array().to_vec()), + )); + } + + if let Some(witness_utxo) = &input.witness_utxo { + let mut witness_node = Node::new("witness_utxo", Primitive::None); + witness_node.add_child(Node::new( + "value", + Primitive::U64(witness_utxo.value.to_sat()), + )); + witness_node.add_child(Node::new( + "script_pubkey", + Primitive::Buffer(witness_utxo.script_pubkey.as_bytes().to_vec()), + )); + witness_node.add_child(Node::new( + "address", + Primitive::String( + bitcoin::Address::from_script(&witness_utxo.script_pubkey, network) + .map(|a| a.to_string()) + .unwrap_or_else(|_| "".to_string()), + ), + )); + input_node.add_child(witness_node); + } + + if let Some(redeem_script) = &input.redeem_script { + input_node.add_child(script_buf_to_node("redeem_script", redeem_script)); + } + + if let Some(witness_script) = &input.witness_script { + input_node.add_child(script_buf_to_node("witness_script", witness_script)) + } + + let mut sigs_node = Node::new( + "signatures", + Primitive::U64(input.partial_sigs.len() as u64), + ); + for (i, (pubkey, sig)) in input.partial_sigs.iter().enumerate() { + let mut sig_node = Node::new(format!("{}", i), Primitive::None); + sig_node.add_child(Node::new("pubkey", Primitive::Buffer(pubkey.to_bytes()))); + sig_node.add_child(Node::new("signature", Primitive::Buffer(sig.to_vec()))); + sigs_node.add_child(sig_node); + } + + if !input.partial_sigs.is_empty() { + input_node.add_child(sigs_node); + } + + if let Some(sighash) = &input.sighash_type { + input_node.add_child(Node::new("sighash_type", Primitive::U32(sighash.to_u32()))); + input_node.add_child(Node::new( + "sighash_type", + Primitive::String(sighash.to_string()), + )); + } + + input_node.extend(bip32_derivations_to_nodes(&input.bip32_derivation)); + + if input.proprietary.len() > 0 { + let mut prop_node = Node::new( + "proprietary", + Primitive::U64(input.proprietary.len() as u64), + ); + prop_node.extend(proprietary_to_nodes(&input.proprietary)); + input_node.add_child(prop_node); + } + + inputs_node.add_child(input_node); + } + + psbt_node.add_child(inputs_node); + + let mut outputs_node = Node::new("outputs", Primitive::U64(psbt.outputs.len() as u64)); + for (i, output) in psbt.outputs.iter().enumerate() { + let mut output_node = Node::new(format!("{}", i), Primitive::None); + + if let Some(script) = &output.redeem_script { + output_node.add_child(script_buf_to_node("redeem_script", script)); + } + + if let Some(script) = &output.witness_script { + output_node.add_child(script_buf_to_node("witness_script", script)); + } + + if output.proprietary.len() > 0 { + let mut prop_node = Node::new( + "proprietary", + Primitive::U64(output.proprietary.len() as u64), + ); + prop_node.extend(proprietary_to_nodes(&output.proprietary)); + output_node.add_child(prop_node); + } + + output_node.extend(bip32_derivations_to_nodes(&output.bip32_derivation)); + + outputs_node.add_child(output_node); + } + + psbt_node.add_child(outputs_node); + + psbt_node +} + +pub fn tx_to_node(tx: &Transaction, network: bitcoin::Network) -> Node { + let mut tx_node = Node::new("tx", Primitive::None); + + tx_node.add_child(Node::new("version", Primitive::I32(tx.version.0))); + tx_node.add_child(Node::new( + "lock_time", + Primitive::U32(tx.lock_time.to_consensus_u32()), + )); + tx_node.add_child(Node::new( + "txid", + Primitive::Buffer(tx.compute_txid().to_byte_array().to_vec()), + )); + tx_node.add_child(Node::new( + "ntxid", + Primitive::Buffer(tx.compute_ntxid().to_byte_array().to_vec()), + )); + tx_node.add_child(Node::new( + "wtxid", + Primitive::Buffer(tx.compute_wtxid().to_byte_array().to_vec()), + )); + + let mut inputs_node = Node::new("inputs", Primitive::U64(tx.input.len() as u64)); + for (i, input) in tx.input.iter().enumerate() { + let mut input_node = Node::new(format!("input_{}", i), Primitive::None); + + input_node.add_child(Node::new( + "prev_txid", + Primitive::Buffer(input.previous_output.txid.to_byte_array().to_vec()), + )); + input_node.add_child(Node::new( + "prev_vout", + Primitive::U32(input.previous_output.vout), + )); + input_node.add_child(Node::new( + "sequence", + Primitive::U32(input.sequence.to_consensus_u32()), + )); + + input_node.add_child(Node::new( + "script_sig", + Primitive::Buffer(input.script_sig.as_bytes().to_vec()), + )); + + if !input.witness.is_empty() { + let mut witness_node = Node::new("witness", Primitive::U64(input.witness.len() as u64)); + + for (j, item) in input.witness.iter().enumerate() { + witness_node.add_child(Node::new( + format!("item_{}", j), + Primitive::Buffer(item.to_vec()), + )); + } + + input_node.add_child(witness_node); + } + + inputs_node.add_child(input_node); + } + + tx_node.add_child(inputs_node); + + let mut outputs_node = Node::new("outputs", Primitive::U64(tx.output.len() as u64)); + for (i, output) in tx.output.iter().enumerate() { + let mut output_node = Node::new(format!("output_{}", i), Primitive::None); + + output_node.add_child(Node::new("value", Primitive::U64(output.value.to_sat()))); + + output_node.add_child(Node::new( + "script_pubkey", + Primitive::Buffer(output.script_pubkey.as_bytes().to_vec()), + )); + + if let Ok(address) = bitcoin::Address::from_script(&output.script_pubkey, network) { + output_node.add_child(Node::new("address", Primitive::String(address.to_string()))); + } + + outputs_node.add_child(output_node); + } + + tx_node.add_child(outputs_node); + + tx_node +} + +pub fn parse_psbt_bytes_internal(bytes: &[u8]) -> Result { + Psbt::deserialize(bytes) + .map(|psbt| psbt_to_node(&psbt, Network::Bitcoin)) + .map_err(|e| e.to_string()) +} + +pub fn parse_tx_bytes_internal(bytes: &[u8]) -> Result { + Transaction::consensus_decode(&mut &bytes[..]) + .map(|tx| tx_to_node(&tx, Network::Bitcoin)) + .map_err(|e| e.to_string()) +} diff --git a/packages/wasm-utxo/cli/src/psbt.rs b/packages/wasm-utxo/cli/src/psbt.rs new file mode 100644 index 00000000..bff5b37b --- /dev/null +++ b/packages/wasm-utxo/cli/src/psbt.rs @@ -0,0 +1,76 @@ +use anyhow::{Context, Result}; +use base64::Engine; +use clap::Subcommand; +use std::fs; +use std::io::{self, Read}; +use std::path::PathBuf; + +use crate::format::{render_tree_with_scheme, ColorScheme}; +use crate::parse_node::parse_psbt_bytes_internal; + +fn decode_input(raw_bytes: &[u8]) -> Result> { + // Try to interpret as text first (for base64/hex encoded input) + if let Ok(text) = std::str::from_utf8(raw_bytes) { + let trimmed = text.trim(); + + // Try base64 first (more common for PSBTs) + if let Ok(decoded) = base64::engine::general_purpose::STANDARD.decode(trimmed) { + return Ok(decoded); + } + + // Try hex + if let Ok(decoded) = hex::decode(trimmed) { + return Ok(decoded); + } + } + + // Fall back to raw bytes + Ok(raw_bytes.to_vec()) +} + +#[derive(Subcommand)] +pub enum PsbtCommand { + /// Parse a PSBT file and display its contents + Parse { + /// Path to the PSBT file (use '-' to read from stdin) + path: PathBuf, + /// Disable colored output + #[arg(long)] + no_color: bool, + }, +} + +pub fn handle_command(command: PsbtCommand) -> Result<()> { + match command { + PsbtCommand::Parse { path, no_color } => { + let raw_bytes = if path.to_str() == Some("-") { + // Read from stdin + let mut buffer = Vec::new(); + io::stdin() + .read_to_end(&mut buffer) + .context("Failed to read from stdin")?; + buffer + } else { + // Read from file + fs::read(&path) + .with_context(|| format!("Failed to read PSBT file: {}", path.display()))? + }; + + // Decode input (auto-detect base64, hex, or raw bytes) + let bytes = decode_input(&raw_bytes)?; + + let node = parse_psbt_bytes_internal(&bytes) + .map_err(|e| anyhow::anyhow!("Failed to parse PSBT: {}", e))?; + + let color_scheme = if no_color { + ColorScheme::no_color() + } else { + ColorScheme::default() + }; + + render_tree_with_scheme(&node, &color_scheme)?; + + Ok(()) + } + } +} diff --git a/packages/wasm-utxo/cli/tests/fixtures/complex_tree.txt b/packages/wasm-utxo/cli/tests/fixtures/complex_tree.txt new file mode 100644 index 00000000..fc673204 --- /dev/null +++ b/packages/wasm-utxo/cli/tests/fixtures/complex_tree.txt @@ -0,0 +1,13 @@ +person: None +├─ name: John Doe +├─ age: 35u8 +├─ address: None +│ ├─ street: 123 Main St +│ ├─ city: Anytown +│ └─ zip: 12345u16 +├─ phones: None +│ ├─ home: 555-1234 +│ └─ work: 555-5678 +└─ account: None + ├─ number: 9876543210123456 + └─ balance: 5000i32 diff --git a/packages/wasm-utxo/cli/tests/fixtures/large_buffer.txt b/packages/wasm-utxo/cli/tests/fixtures/large_buffer.txt new file mode 100644 index 00000000..aa16188f --- /dev/null +++ b/packages/wasm-utxo/cli/tests/fixtures/large_buffer.txt @@ -0,0 +1 @@ +large: 000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f60616263 (100 bytes) diff --git a/packages/wasm-utxo/cli/tests/fixtures/numeric_types.txt b/packages/wasm-utxo/cli/tests/fixtures/numeric_types.txt new file mode 100644 index 00000000..ed94dc68 --- /dev/null +++ b/packages/wasm-utxo/cli/tests/fixtures/numeric_types.txt @@ -0,0 +1,14 @@ +numbers: None +├─ i8_min: -128i8 +├─ i8_max: 127i8 +├─ i16_min: -32768i16 +├─ i16_max: 32767i16 +├─ i32_min: -2147483648i32 +├─ i32_max: 2147483647i32 +├─ i64_min: -9223372036854775808i64 +├─ i64_max: 9223372036854775807i64 +├─ u8_max: 255u8 +├─ u16_max: 65535u16 +├─ u32_max: 4294967295u32 +├─ u64_max: 18446744073709551615u64 +└─ bigint: 12345678901234567890123456789012345678901234567890 diff --git a/packages/wasm-utxo/cli/tests/fixtures/simple_tree.txt b/packages/wasm-utxo/cli/tests/fixtures/simple_tree.txt new file mode 100644 index 00000000..875248e1 --- /dev/null +++ b/packages/wasm-utxo/cli/tests/fixtures/simple_tree.txt @@ -0,0 +1,4 @@ +person: None +├─ name: Alice +├─ age: 30u8 +└─ active: true diff --git a/packages/wasm-utxo/cli/tests/fixtures/small_buffer.txt b/packages/wasm-utxo/cli/tests/fixtures/small_buffer.txt new file mode 100644 index 00000000..ad0fee8e --- /dev/null +++ b/packages/wasm-utxo/cli/tests/fixtures/small_buffer.txt @@ -0,0 +1 @@ +small: 01020304 (4 bytes)