From 4fcf7ac89e440d34207e2c190533d94955a1b808 Mon Sep 17 00:00:00 2001 From: Alekos Filini Date: Mon, 20 Jul 2020 15:51:57 +0200 Subject: [PATCH] Make the blockchain interface async again on wasm32-unknown-unknown The procedural macro `#[maybe_async]` makes a method or every method of a trait "async" whenever the target_arch is `wasm32`, and leaves them untouched on every other platform. The macro `maybe_await!($e:expr)` can be used to call `maybe_async` methods on multi-platform code: it expands to `$e` on non-wasm32 platforms and to `$e.await` on wasm32. The macro `await_or_block!($e:expr)` can be used to contain async code as much as possible: it expands to `tokio::runtime::Runtime::new().unwrap().block_on($e)` on non-wasm32 platforms, and to `$e.await` on wasm32. --- .travis.yml | 10 ++- Cargo.toml | 11 +++- macros/Cargo.toml | 15 +++++ macros/src/lib.rs | 134 ++++++++++++++++++++++++++++++++++++++ src/blockchain/esplora.rs | 119 ++++++++++++++++----------------- src/blockchain/mod.rs | 3 +- src/blockchain/utils.rs | 25 ++++--- src/cli.rs | 5 +- src/lib.rs | 6 ++ src/wallet/mod.rs | 11 ++-- 10 files changed, 254 insertions(+), 85 deletions(-) create mode 100644 macros/Cargo.toml create mode 100644 macros/src/lib.rs diff --git a/.travis.yml b/.travis.yml index fda35b65..3c49b95f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,15 @@ language: rust rust: - stable -# - 1.31.0 -# - 1.22.0 before_script: + # Install a recent version of clang that supports wasm32 + - wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key | sudo apt-key add - + - sudo apt-add-repository "deb http://apt.llvm.org/xenial/ llvm-toolchain-xenial-10 main" + - sudo apt-get update + - sudo apt-get install -y clang-10 libc6-dev-i386 + # Install the required components and targets - rustup component add rustfmt + - rustup target add wasm32-unknown-unknown script: - cargo fmt -- --check --verbose - cargo test --verbose --all @@ -13,6 +18,7 @@ script: - cargo build --verbose --no-default-features --features=minimal,esplora - cargo build --verbose --no-default-features --features=key-value-db - cargo build --verbose --no-default-features --features=electrum + - CC="clang-10" CFLAGS="-I/usr/include" cargo build --verbose --no-default-features --features=cli-utils,esplora --target=wasm32-unknown-unknown notifications: email: false diff --git a/Cargo.toml b/Cargo.toml index 65be202d..c2b6f29a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ edition = "2018" authors = ["Riccardo Casatta ", "Alekos Filini "] [dependencies] +magical-macros = { path = "./macros" } log = "^0.4" bitcoin = { version = "0.23", features = ["use-serde"] } miniscript = { version = "1.0" } @@ -15,17 +16,23 @@ serde_json = { version = "^1.0" } sled = { version = "0.31.0", optional = true } electrum-client = { version = "0.2.0-beta.1", optional = true } reqwest = { version = "0.10", optional = true, features = ["json"] } -tokio = { version = "0.2", optional = true, features = ["rt-core"] } futures = { version = "0.3", optional = true } clap = { version = "2.33", optional = true } base64 = { version = "^0.11", optional = true } +# Platform-specific dependencies +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +tokio = { version = "0.2", features = ["rt-core"] } + +[target.'cfg(target_arch = "wasm32")'.dependencies] +async-trait = "0.1" + [features] minimal = [] compiler = ["miniscript/compiler"] default = ["key-value-db", "electrum"] electrum = ["electrum-client"] -esplora = ["reqwest", "futures", "tokio"] +esplora = ["reqwest", "futures"] key-value-db = ["sled"] cli-utils = ["clap", "base64"] diff --git a/macros/Cargo.toml b/macros/Cargo.toml new file mode 100644 index 00000000..8c581e6b --- /dev/null +++ b/macros/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "magical-macros" +version = "0.1.0" +authors = ["Alekos Filini "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +syn = { version = "1.0", features = ["parsing"] } +proc-macro2 = "1.0" +quote = "1.0" + +[lib] +proc-macro = true diff --git a/macros/src/lib.rs b/macros/src/lib.rs new file mode 100644 index 00000000..c7d984d6 --- /dev/null +++ b/macros/src/lib.rs @@ -0,0 +1,134 @@ +#[macro_use] +extern crate quote; + +use proc_macro::TokenStream; + +use syn::spanned::Spanned; +use syn::{parse, ImplItemMethod, ItemImpl, ItemTrait, Token}; + +fn add_async_trait(mut parsed: ItemTrait) -> TokenStream { + let output = quote! { + #[cfg(not(target_arch = "wasm32"))] + #parsed + }; + + for mut item in &mut parsed.items { + if let syn::TraitItem::Method(m) = &mut item { + m.sig.asyncness = Some(Token![async](m.span())); + } + } + + let output = quote! { + #output + + #[cfg(target_arch = "wasm32")] + #[async_trait(?Send)] + #parsed + }; + + output.into() +} + +fn add_async_method(mut parsed: ImplItemMethod) -> TokenStream { + let output = quote! { + #[cfg(not(target_arch = "wasm32"))] + #parsed + }; + + parsed.sig.asyncness = Some(Token![async](parsed.span())); + + let output = quote! { + #output + + #[cfg(target_arch = "wasm32")] + #parsed + }; + + output.into() +} + +fn add_async_impl_trait(mut parsed: ItemImpl) -> TokenStream { + let output = quote! { + #[cfg(not(target_arch = "wasm32"))] + #parsed + }; + + for mut item in &mut parsed.items { + if let syn::ImplItem::Method(m) = &mut item { + m.sig.asyncness = Some(Token![async](m.span())); + } + } + + let output = quote! { + #output + + #[cfg(target_arch = "wasm32")] + #[async_trait(?Send)] + #parsed + }; + + output.into() +} + +/// Makes a method or every method of a trait "async" only if the target_arch is "wasm32" +/// +/// Requires the `async-trait` crate as a dependency whenever this attribute is used on a trait +/// definition or trait implementation. +#[proc_macro_attribute] +pub fn maybe_async(_attr: TokenStream, item: TokenStream) -> TokenStream { + if let Ok(parsed) = parse(item.clone()) { + add_async_trait(parsed) + } else if let Ok(parsed) = parse(item.clone()) { + add_async_method(parsed) + } else if let Ok(parsed) = parse(item) { + add_async_impl_trait(parsed) + } else { + (quote! { + compile_error!("#[maybe_async] can only be used on methods, trait or trait impl blocks") + }).into() + } +} + +/// Awaits if target_arch is "wasm32", does nothing otherwise +#[proc_macro] +pub fn maybe_await(expr: TokenStream) -> TokenStream { + let expr: proc_macro2::TokenStream = expr.into(); + let quoted = quote! { + { + #[cfg(not(target_arch = "wasm32"))] + { + #expr + } + + #[cfg(target_arch = "wasm32")] + { + #expr.await + } + } + }; + + quoted.into() +} + +/// Awaits if target_arch is "wasm32", uses `futures::executor::block_on()` otherwise +/// +/// Requires the `tokio` crate as a dependecy with `rt-core` or `rt-threaded` to build on non-wasm32 platforms. +#[proc_macro] +pub fn await_or_block(expr: TokenStream) -> TokenStream { + let expr: proc_macro2::TokenStream = expr.into(); + let quoted = quote! { + { + #[cfg(not(target_arch = "wasm32"))] + { + tokio::runtime::Runtime::new().unwrap().block_on(#expr) + } + + #[cfg(target_arch = "wasm32")] + { + #expr.await + } + } + }; + + quoted.into() +} diff --git a/src/blockchain/esplora.rs b/src/blockchain/esplora.rs index 150b4cf9..6ba6a596 100644 --- a/src/blockchain/esplora.rs +++ b/src/blockchain/esplora.rs @@ -1,10 +1,7 @@ use std::collections::HashSet; -use std::sync::Mutex; use futures::stream::{self, StreamExt, TryStreamExt}; -use tokio::runtime::Runtime; - #[allow(unused_imports)] use log::{debug, error, info, trace}; @@ -26,11 +23,8 @@ use crate::error::Error; pub struct UrlClient { url: String, // We use the async client instead of the blocking one because it automatically uses `fetch` - // when the target platform is wasm32. For some reason the blocking client doesn't, so we are - // stuck with this + // when the target platform is wasm32. client: Client, - - runtime: Mutex, } #[derive(Debug)] @@ -47,8 +41,6 @@ impl EsploraBlockchain { EsploraBlockchain(Some(UrlClient { url: base_url.to_string(), client: Client::new(), - - runtime: Mutex::new(Runtime::new().unwrap()), })) } } @@ -63,6 +55,7 @@ impl Blockchain for EsploraBlockchain { } } +#[maybe_async] impl OnlineBlockchain for EsploraBlockchain { fn get_capabilities(&self) -> HashSet { vec![Capability::FullHistory, Capability::GetAnyTx] @@ -76,26 +69,35 @@ impl OnlineBlockchain for EsploraBlockchain { database: &mut D, progress_update: P, ) -> Result<(), Error> { - self.0 - .as_mut() - .ok_or(Error::OfflineClient)? - .electrum_like_setup(stop_gap, database, progress_update) - } - - fn get_tx(&mut self, txid: &Txid) -> Result, Error> { - Ok(self.0.as_mut().ok_or(Error::OfflineClient)?._get_tx(txid)?) - } - - fn broadcast(&mut self, tx: &Transaction) -> Result<(), Error> { - Ok(self + maybe_await!(self .0 .as_mut() .ok_or(Error::OfflineClient)? - ._broadcast(tx)?) + .electrum_like_setup(stop_gap, database, progress_update)) + } + + fn get_tx(&mut self, txid: &Txid) -> Result, Error> { + Ok(await_or_block!(self + .0 + .as_mut() + .ok_or(Error::OfflineClient)? + ._get_tx(txid))?) + } + + fn broadcast(&mut self, tx: &Transaction) -> Result<(), Error> { + Ok(await_or_block!(self + .0 + .as_mut() + .ok_or(Error::OfflineClient)? + ._broadcast(tx))?) } fn get_height(&mut self) -> Result { - Ok(self.0.as_mut().ok_or(Error::OfflineClient)?._get_height()?) + Ok(await_or_block!(self + .0 + .as_mut() + .ok_or(Error::OfflineClient)? + ._get_height())?) } } @@ -104,54 +106,39 @@ impl UrlClient { sha256::Hash::hash(script.as_bytes()).into_inner().to_hex() } - fn _get_tx(&self, txid: &Txid) -> Result, EsploraError> { - let resp = self.runtime.lock().unwrap().block_on( - self.client - .get(&format!("{}/api/tx/{}/raw", self.url, txid)) - .send(), - )?; + async fn _get_tx(&self, txid: &Txid) -> Result, EsploraError> { + let resp = self + .client + .get(&format!("{}/api/tx/{}/raw", self.url, txid)) + .send() + .await?; if let StatusCode::NOT_FOUND = resp.status() { return Ok(None); } - Ok(Some(deserialize( - &self - .runtime - .lock() - .unwrap() - .block_on(resp.error_for_status()?.bytes())?, - )?)) + Ok(Some(deserialize(&resp.error_for_status()?.bytes().await?)?)) } - fn _broadcast(&self, transaction: &Transaction) -> Result<(), EsploraError> { - self.runtime - .lock() - .unwrap() - .block_on( - self.client - .post(&format!("{}/api/tx", self.url)) - .body(serialize(transaction).to_hex()) - .send(), - )? + async fn _broadcast(&self, transaction: &Transaction) -> Result<(), EsploraError> { + self.client + .post(&format!("{}/api/tx", self.url)) + .body(serialize(transaction).to_hex()) + .send() + .await? .error_for_status()?; Ok(()) } - fn _get_height(&self) -> Result { - let req = self.runtime.lock().unwrap().block_on( - self.client - .get(&format!("{}/api/blocks/tip/height", self.url)) - .send(), - )?; + async fn _get_height(&self) -> Result { + let req = self + .client + .get(&format!("{}/api/blocks/tip/height", self.url)) + .send() + .await?; - Ok(self - .runtime - .lock() - .unwrap() - .block_on(req.error_for_status()?.text())? - .parse()?) + Ok(req.error_for_status()?.text().await?.parse()?) } async fn _script_get_history( @@ -247,34 +234,38 @@ impl UrlClient { } } +#[maybe_async] impl ElectrumLikeSync for UrlClient { fn els_batch_script_get_history<'s, I: IntoIterator>( &mut self, scripts: I, ) -> Result>, Error> { - self.runtime.lock().unwrap().block_on(async { + let future = async { Ok(stream::iter(scripts) .then(|script| self._script_get_history(&script)) .try_collect() .await?) - }) + }; + + await_or_block!(future) } fn els_batch_script_list_unspent<'s, I: IntoIterator>( &mut self, scripts: I, ) -> Result>, Error> { - self.runtime.lock().unwrap().block_on(async { + let future = async { Ok(stream::iter(scripts) .then(|script| self._script_list_unspent(&script)) .try_collect() .await?) - }) + }; + + await_or_block!(future) } fn els_transaction_get(&mut self, txid: &Txid) -> Result { - Ok(self - ._get_tx(txid)? + Ok(await_or_block!(self._get_tx(txid))? .ok_or_else(|| EsploraError::TransactionNotFound(*txid))?) } } diff --git a/src/blockchain/mod.rs b/src/blockchain/mod.rs index 3e13d5f9..2a250515 100644 --- a/src/blockchain/mod.rs +++ b/src/blockchain/mod.rs @@ -41,6 +41,7 @@ impl Blockchain for OfflineBlockchain { } } +#[maybe_async] pub trait OnlineBlockchain: Blockchain { fn get_capabilities(&self) -> HashSet; @@ -56,7 +57,7 @@ pub trait OnlineBlockchain: Blockchain { database: &mut D, progress_update: P, ) -> Result<(), Error> { - self.setup(stop_gap, database, progress_update) + maybe_await!(self.setup(stop_gap, database, progress_update)) } fn get_tx(&mut self, txid: &Txid) -> Result, Error>; diff --git a/src/blockchain/utils.rs b/src/blockchain/utils.rs index e8e3769c..76bc5e70 100644 --- a/src/blockchain/utils.rs +++ b/src/blockchain/utils.rs @@ -27,6 +27,7 @@ pub struct ELSListUnspentRes { } /// Implements the synchronization logic for an Electrum-like client. +#[maybe_async] pub trait ElectrumLikeSync { fn els_batch_script_get_history<'s, I: IntoIterator>( &mut self, @@ -85,7 +86,7 @@ pub trait ElectrumLikeSync { let until = cmp::min(to_check_later.len(), batch_query_size); let chunk: Vec