diff --git a/CHANGELOG.md b/CHANGELOG.md index de884f0..49cccf3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Change rpc `--skip-blocks` option to `--start-time` which specifies time initial sync will start scanning from. - Add new `bdk-cli node []` to control the backend node deployed by `regtest-*` features. - Add an integration testing framework in `src/tests/integration.rs`. This framework uses the `regtest-*` feature to run automated testing with bdk-cli. +- Add a module `wasm` containing objects to use bdk-cli from web assembly ## [0.5.0] diff --git a/Cargo.lock b/Cargo.lock index 3075154..ff86dcc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -138,12 +138,19 @@ dependencies = [ "electrsd", "env_logger", "fd-lock", + "js-sys", "log", + "rand 0.6.5", "regex", "rustyline", + "secp256k1", + "serde", "serde_json", "structopt", "tokio", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-logger", "zeroize", ] @@ -1047,9 +1054,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.59" +version = "0.3.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "258451ab10b34f8af53416d1fdab72c22e805f0c92a1136d59470ec0b11138b2" +checksum = "a38fc24e30fd564ce974c02bf1d337caddff65be6cc4735a1f7eab22a7440f04" dependencies = [ "wasm-bindgen", ] @@ -1589,6 +1596,7 @@ dependencies = [ "libc", "rand_core 0.4.2", "rdrand", + "wasm-bindgen", "winapi", ] @@ -2479,23 +2487,25 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.82" +version = "0.2.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7652e3f6c4706c8d9cd54832c4a4ccb9b5336e2c3bd154d5cccfbf1c1f5f7d" +checksum = "25f1af7423d8588a3d840681122e72e6a24ddbcb3f0ec385cac0d12d24256c06" dependencies = [ "cfg-if", + "serde", + "serde_json", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.82" +version = "0.2.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "662cd44805586bd52971b9586b1df85cdbbd9112e4ef4d8f41559c334dc6ac3f" +checksum = "8b21c0df030f5a177f3cba22e9bc4322695ec43e7257d865302900290bcdedca" dependencies = [ "bumpalo", + "lazy_static", "log", - "once_cell", "proc-macro2", "quote", "syn", @@ -2504,9 +2514,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.32" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa76fb221a1f8acddf5b54ace85912606980ad661ac7a503b4570ffd3a624dad" +checksum = "2eb6ec270a31b1d3c7e266b999739109abce8b6c87e4b31fcfcd788b65267395" dependencies = [ "cfg-if", "js-sys", @@ -2516,9 +2526,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.82" +version = "0.2.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b260f13d3012071dfb1512849c033b1925038373aea48ced3012c09df952c602" +checksum = "2f4203d69e40a52ee523b2529a773d5ffc1dc0071801c87b3d270b471b80ed01" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2526,9 +2536,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.82" +version = "0.2.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5be8e654bdd9b79216c2929ab90721aa82faf65c48cdf08bdc4e7f51357b80da" +checksum = "bfa8a30d46208db204854cadbb5d4baf5fcf8071ba5bf48190c3e59937962ebc" dependencies = [ "proc-macro2", "quote", @@ -2539,15 +2549,26 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.82" +version = "0.2.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d958d035c4438e28c70e4321a2911302f10135ce78a9c7834c0cab4123d06a2" + +[[package]] +name = "wasm-logger" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6598dd0bd3c7d51095ff6531a5b23e02acdc81804e30d8f07afb77b7215a140a" +checksum = "074649a66bb306c8f2068c9016395fa65d8e08d2affcbf95acf3c24c3ab19718" +dependencies = [ + "log", + "wasm-bindgen", + "web-sys", +] [[package]] name = "web-sys" -version = "0.3.59" +version = "0.3.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed055ab27f941423197eb86b2035720b1a3ce40504df082cac2ecc6ed73335a1" +checksum = "c060b319f29dd25724f09a2ba1418f142f539b2be99fbf4d2d5a8f7330afb8eb" dependencies = [ "js-sys", "wasm-bindgen", diff --git a/Cargo.toml b/Cargo.toml index ba7a46b..df206dc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,7 +27,20 @@ fd-lock = { version = "=3.0.2", optional = true } regex = { version = "1", optional = true } bdk-reserves = { version = "0.22", optional = true } electrsd = { version= "0.19", features = ["bitcoind_22_0"], optional = true} -tokio = { version = "1", features = ["rt", "macros", "rt-multi-thread"], optional = true } + +# Platform-specific dependencies +[target.'cfg(target_arch = "wasm32")'.dependencies] +wasm-bindgen = { version = "=0.2.79", features = ["serde-serialize"] } +wasm-bindgen-futures = { version = "0.4" } +js-sys = "=0.3.56" +wasm-logger = "0.2.0" +secp256k1 = { version = "0.22.0", default-features = false } +rand = { version = "^0.6", features = ["wasm-bindgen"] } +serde = { version = "^1.0", features = ["derive"] } +regex = { version = "1" } + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +tokio = { version = "1", features = ["rt", "macros", "rt-multi-thread"] } [features] default = ["repl", "sqlite-db"] @@ -45,7 +58,7 @@ electrum = ["bdk/electrum"] compact_filters = ["bdk/compact_filters"] esplora = [] esplora-ureq = ["esplora", "bdk/use-esplora-ureq"] -async-interface = ["bdk/async-interface", "tokio"] +async-interface = ["bdk/async-interface"] esplora-reqwest = ["esplora", "bdk/use-esplora-reqwest", "bdk/reqwest-default-tls", "async-interface"] # Use this to consensus verify transactions at sync time @@ -68,4 +81,4 @@ regtest-bitcoin = ["regtest-node" , "rpc"] regtest-electrum = ["regtest-node", "electrum", "electrsd/electrs_0_8_10"] #TODO: Check why esplora in electrsd isn't working. #regtest-esplora-ureq = ["regtest-node", "esplora-ureq", "electrsd/esplora_a33e97e1"] -#regtest-esplora-reqwest = ["regtest-node", "esplora-reqwest", "electrsd/esplora_a33e97e1"] \ No newline at end of file +#regtest-esplora-reqwest = ["regtest-node", "esplora-reqwest", "electrsd/esplora_a33e97e1"] diff --git a/src/commands.rs b/src/commands.rs index fafa5ad..ee675fe 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -537,7 +537,7 @@ pub enum KeySubCommand { }, } -#[cfg(feature = "repl")] +#[cfg(any(feature = "repl", target_arch = "wasm32"))] #[derive(Debug, StructOpt, Clone, PartialEq)] #[structopt(global_settings =&[AppSettings::NoBinaryName], rename_all = "lower")] pub enum ReplSubCommand { diff --git a/src/main.rs b/src/main.rs index 66e53f9..5f447bf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,6 +14,9 @@ mod commands; mod handlers; mod nodes; mod utils; +#[cfg(target_arch = "wasm32")] +mod wasm; + use bitcoin::Network; use log::{debug, error, warn}; @@ -24,10 +27,11 @@ use bdk::{bitcoin, Error}; use bdk_macros::{maybe_async, maybe_await}; use structopt::StructOpt; -#[cfg(feature = "repl")] +#[cfg(any(feature = "repl", target_arch = "wasm32"))] const REPL_LINE_SPLIT_REGEX: &str = r#""([^"]*)"|'([^']*)'|([\w\-]+)"#; #[maybe_async] +#[cfg(not(target_arch = "wasm32"))] #[cfg_attr(feature = "async-interface", tokio::main)] fn main() { env_logger::init(); @@ -50,3 +54,7 @@ fn main() { }, } } + +// wasm32 requires a non-async main +#[cfg(target_arch = "wasm32")] +fn main() {} diff --git a/src/wasm.rs b/src/wasm.rs new file mode 100644 index 0000000..371a1a7 --- /dev/null +++ b/src/wasm.rs @@ -0,0 +1,204 @@ +use crate::commands::*; +use crate::handlers::*; +use crate::nodes::Nodes; +use crate::utils::*; +use bdk::*; + +use bitcoin::*; + +use bdk::blockchain::AnyBlockchain; +use bdk::database::AnyDatabase; +use js_sys::Promise; +use regex::Regex; +use std::error::Error; +use std::ops::Deref; +use std::path::PathBuf; +use std::rc::Rc; +use std::str::FromStr; +use structopt::StructOpt; +use wasm_bindgen::prelude::*; +use wasm_bindgen_futures::future_to_promise; + +#[cfg(feature = "compiler")] +use bdk::keys::{GeneratableDefaultOptions, GeneratedKey}; +#[cfg(feature = "compiler")] +use bdk::miniscript::{self, policy::Concrete, Descriptor, TranslatePk}; +#[cfg(feature = "compiler")] +use serde::Deserialize; + +#[wasm_bindgen] +pub struct WasmWallet { + wallet: Rc>, + wallet_opts: Rc, + blockchain: Rc, + network: Network, +} + +#[wasm_bindgen] +pub fn log_init() { + wasm_logger::init(wasm_logger::Config::default()); +} + +#[wasm_bindgen] +impl WasmWallet { + #[wasm_bindgen(constructor)] + pub fn new(network: JsValue, wallet_opts: Vec) -> Result { + fn new_inner( + network: JsValue, + wallet_opts: Vec, + ) -> Result> { + // Both open_database and new_blockchain need a home path to be passed + // in, even tho it won't be used + let dummy_home_dir = PathBuf::new(); + let wallet_opts = wallet_opts + .into_iter() + .map(|a| a.as_string().expect("Invalid type")); + let wallet_opts: WalletOpts = WalletOpts::from_iter_safe(wallet_opts)?; + let network = network + .as_string() + .ok_or_else(|| Box::::from("InvalidScriptType"))?; + let network = Network::from_str(&network)?; + let wallet_opts = maybe_descriptor_wallet_name(wallet_opts, network)?; + let database = open_database(&wallet_opts, &dummy_home_dir)?; + let wallet = new_wallet(network, &wallet_opts, database)?; + let blockchain = new_blockchain(network, &wallet_opts, &Nodes::None, &dummy_home_dir)?; + Ok(WasmWallet { + wallet: Rc::new(wallet), + wallet_opts: Rc::new(wallet_opts), + blockchain: Rc::new(blockchain), + network, + }) + } + + new_inner(network, wallet_opts).map_err(|e| e.to_string().into()) + } + + pub fn run_command(&self, command: String) -> Promise { + let wallet = Rc::clone(&self.wallet); + let wallet_opts = Rc::clone(&self.wallet_opts); + let blockchain = Rc::clone(&self.blockchain); + let network = self.network; + + async fn run_command_inner( + command: String, + wallet: Rc>, + wallet_opts: Rc, + blockchain: Rc, + network: Network, + ) -> Result> { + let split_regex = Regex::new(crate::REPL_LINE_SPLIT_REGEX)?; + let split_line: Vec<&str> = split_regex + .captures_iter(&command) + .map(|c| { + Ok(c.get(1) + .or_else(|| c.get(2)) + .or_else(|| c.get(3)) + .ok_or_else(|| "Invalid commands".to_string())? + .as_str()) + }) + .collect::, String>>()?; + let repl_subcommand = ReplSubCommand::from_iter_safe(split_line)?; + log::debug!("repl_subcommand = {:?}", repl_subcommand); + + let result = match repl_subcommand { + ReplSubCommand::Wallet { + subcommand: WalletSubCommand::OnlineWalletSubCommand(online_subcommand), + } => { + handle_online_wallet_subcommand(&wallet, blockchain.deref(), online_subcommand) + .await? + } + ReplSubCommand::Wallet { + subcommand: WalletSubCommand::OfflineWalletSubCommand(offline_subcommand), + } => handle_offline_wallet_subcommand(&wallet, &wallet_opts, offline_subcommand)?, + ReplSubCommand::Key { subcommand } => handle_key_subcommand(network, subcommand)?, + ReplSubCommand::Exit => return Ok(serde_json::Value::Null), + }; + + Ok(result) + } + + // The promise returned contains a Result + future_to_promise(async move { + run_command_inner(command, wallet, wallet_opts, blockchain, network) + .await + .map(|v| JsValue::from_serde(&v).expect("Serde serialization failed")) + .map_err(|e| e.to_string().into()) + }) + } +} + +#[wasm_bindgen] +#[cfg(feature = "compiler")] +pub fn compile(policy: String, aliases: String, script_type: String) -> Result { + fn compile_inner( + policy: String, + aliases: String, + script_type: String, + ) -> Result> { + use std::collections::HashMap; + let aliases: HashMap = serde_json::from_str(&aliases)?; + let aliases: HashMap = aliases + .into_iter() + .map(|(k, v)| (k, v.into_key())) + .collect(); + + let policy = Concrete::::from_str(&policy)?; + + let descriptor = match script_type.as_str() { + "sh" => Descriptor::new_sh(policy.compile()?)?, + "wsh" => Descriptor::new_wsh(policy.compile()?)?, + "sh-wsh" => Descriptor::new_sh_wsh(policy.compile()?)?, + _ => return Err(Box::::from("InvalidScriptType")), + }; + + let descriptor: Result, bdk::Error> = descriptor.translate_pk( + |key| Ok(aliases.get(key).unwrap_or(key).into()), + |key| Ok(aliases.get(key).unwrap_or(key).into()), + ); + let descriptor = descriptor?; + + Ok(format!("{}", descriptor).into()) + } + + compile_inner(policy, aliases, script_type) + .map(|v| JsValue::from_serde(&v).expect("Serde serialization failed")) + .map_err(|e| e.to_string().into()) +} + +#[derive(Debug, Deserialize)] +#[serde(tag = "type", rename_all = "snake_case")] +#[cfg(feature = "compiler")] +enum Alias { + GenWif, + GenExt { extra: String }, + Existing { extra: String }, +} + +#[cfg(feature = "compiler")] +impl Alias { + fn into_key(self) -> String { + match self { + Alias::GenWif => { + let generated: GeneratedKey = + GeneratableDefaultOptions::generate_default().unwrap(); + + let mut key = generated.into_key(); + key.network = Network::Testnet; + + key.to_wif() + } + Alias::GenExt { extra: path } => { + let generated: GeneratedKey< + bitcoin::util::bip32::ExtendedPrivKey, + miniscript::Legacy, + > = GeneratableDefaultOptions::generate_default().unwrap(); + + let mut xprv = generated.into_key(); + xprv.network = Network::Testnet; + + format!("{}{}", xprv, path) + } + Alias::Existing { extra } => extra, + } + } +}