]> Untitled Git - bdk-cli/commitdiff
Add wasm support
authorDaniela Brozzoni <danielabrozzoni@protonmail.com>
Mon, 5 Sep 2022 13:25:53 +0000 (15:25 +0200)
committerDaniela Brozzoni <danielabrozzoni@protonmail.com>
Thu, 22 Sep 2022 10:55:50 +0000 (12:55 +0200)
Add a module "wasm" with utilities to be used in the bdk playground:
- A WasmWallet structure, to create a wallet and run commands
- A compile function, to compile policies into descriptors

CHANGELOG.md
Cargo.lock
Cargo.toml
src/commands.rs
src/main.rs
src/wasm.rs [new file with mode: 0644]

index 99914c522cb0cb565d4bf33d2b2c7f099d0c1776..5fbcbdc72586e1271b6c26e8b1af9f2989533a6d 100644 (file)
@@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - Add new `bdk-cli node <command> [<args>]` 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 possible values for `network` option to improve help message, and fix typo in doc.
+- Add a module `wasm` containing objects to use bdk-cli from web assembly
 
 ## [0.5.0]
 
index 30751544e307302913aef85d7613036264b72fde..ff86dccc8b72ea0cf3fcea0fdf9f3ac956d892b4 100644 (file)
@@ -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",
index ba7a46ba9902bf7b39f090c1b8a7483c1ce29763..df206dc293d1dca11ef790b857f7f359c077658b 100644 (file)
@@ -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"]
index 4764d629f2165295f0cc217b6e409ac4bd229be5..90a74b9adb543580b474a18dbf743b81e85bebab 100644 (file)
@@ -552,7 +552,7 @@ pub enum KeySubCommand {
 }
 
 /// Subcommands available in REPL mode.
-#[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 {
index 2c02d495a1a8a04f9b52c0344b29e0bc78b9cb68..eeebddf9a625efb85dc2f45d3912807bdd24a49a 100644 (file)
@@ -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 (file)
index 0000000..08f674b
--- /dev/null
@@ -0,0 +1,200 @@
+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<AnyDatabase>>,
+    wallet_opts: Rc<WalletOpts>,
+    blockchain: Rc<AnyBlockchain>,
+    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: String, wallet_opts: Vec<JsValue>) -> Result<WasmWallet, String> {
+        fn new_inner(
+            network: String,
+            wallet_opts: Vec<JsValue>,
+        ) -> Result<WasmWallet, Box<dyn Error>> {
+            // 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::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<AnyDatabase>>,
+            wallet_opts: Rc<WalletOpts>,
+            blockchain: Rc<AnyBlockchain>,
+            network: Network,
+        ) -> Result<serde_json::Value, Box<dyn Error>> {
+            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::<Result<Vec<_>, 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)
+        }
+
+        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<JsValue, String> {
+    fn compile_inner(
+        policy: String,
+        aliases: String,
+        script_type: String,
+    ) -> Result<String, Box<dyn Error>> {
+        use std::collections::HashMap;
+        let aliases: HashMap<String, Alias> = serde_json::from_str(&aliases)?;
+        let aliases: HashMap<String, String> = aliases
+            .into_iter()
+            .map(|(k, v)| (k, v.into_key()))
+            .collect();
+
+        let policy = Concrete::<String>::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::<dyn Error>::from("InvalidScriptType")),
+        };
+
+        let descriptor: Result<Descriptor<String>, 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(descriptor.to_string().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<bitcoin::PrivateKey, miniscript::Legacy> =
+                    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,
+        }
+    }
+}