Repository: ergoplatform/sigma-rust (language: rust)

Issue:
[350 SigmaUSD] Node discovery via node's REST API
### Implementation details

Take a list of nodes (seeds) and ask them for their known peers via `/peers/all` REST API endpoint. Repeat the same operation (ask for their known peers) with received nodes skipping the ones we've already asked, keeping a limit of simultaneous requests. Test if the REST API is available on the fetched nodes via `/info` endpoint on port 9053. Filter out nodes that do not have REST API(connection refused, timeout, etc.) Stop when there are no nodes left to ask or a given timeout is reached.
Return the list of discovered nodes without the seeds.

### Rust API
```rust
async fn peer_discovery(seeds: NonEmptyVec<Url>, max_parallel_requests: BoundedU16<1, u16:MAX>, 
    timeout: Duration) -> Result<Vec<Url>, PeerDiscoveryError>
```
Introduce `type NonEmptyVec<T> = BoundedVec<T, 1, usize::MAX>;` in `bounded-vec` crate.
Testing should be done by running a peer discovery on the public nodes list(seeds) shipped with the node - https://github.com/ergoplatform/ergo/blob/f38f3a6d0ab8849871adc54010846c5afb101ff4/src/main/resources/mainnet.conf#L49-L64 using port 9053 (default for REST API).

### Wasm API
```rust
async fn peer_discovery(seeds: Box<[web_sys::Url]>, max_parallel_reqs: u16,
    timeout_sec: u32) -> Result<Box<[web_sys::Url]>, JsValue>
```

### Swift API
```swift
func peerDiscovery(seeds: [URL], maxParallelReqs:UInt16, timeoutSec: UInt32, 
    closure: @escaping (Result<[URL], Error>) -> Void) throws -> RequestHandle
```
The underlying C FFI layer should use string for `URL` representation, passing the array of strings as a pointer and a count.




`/nipopow/proof` REST API endpoint support


Implement `ergo_rest::api::node::get_nipopow_proof_by_header_id` using `/nipopow/proof/{m}/{k}/{headerId}` REST API endpoint.

Add it to the Wasm and C/Swift API. See existing `node::get_info` code for examples.



Discussion / hints:
Going to work on this.
I've got working code that scans over the peers, and it looks like only 2 seeds have an open REST API: "213.239.193.208:9030" and "159.65.11.55:9030". Not a single non-seed peer is responsive on port 9053. I've manually checked things with `curl`. Is this normal?
> I've got working code that scans over the peers, and it looks like only 2 seeds have an open REST API: "213.239.193.208:9030" and "159.65.11.55:9030". Not a single non-seed peer is responsive on port 9053. I've manually checked things with `curl`. Is this normal?

Well, that does not sound good. Are you sure about port 9030, cause I used "213.239.193.208:9053" in tests. 
There is a project I saw in our discord that gathers some stats about the network and I asked them to count the nodes with open REST API and I believe the number they gave me was 17 nodes. I wonder how did they found them.
I am working on the same problem and opened an improvement suggestion for the node: ergoplatform/ergo#1652

Please check it and state your remarks.
> Well, that does not sound good. Are you sure about port 9030, cause I used "213.239.193.208:9053" in tests. 

I made sure to use port 9053 for all the requests. I copied pasted those IP addresses from the seed list.
I searched around Discord and found this thread: https://discord.com/channels/668903786361651200/668930902751182913/920043012384497696

It sounds like they used the handshake protocol to find the nodes. Also @greenhat, is https://ergo.nodespyder.io/ the project you saw on discord?
> I searched around Discord and found this thread: https://discord.com/channels/668903786361651200/668930902751182913/920043012384497696
> 
Here is that answer I mention above - 
https://discord.com/channels/668903786361651200/669989266478202917/925702327103553566
It's from the same `andee` you found.

> It sounds like they used the handshake protocol to find the nodes. 

That's plausible. I mean, I'm pretty sure they did P2P peer discovery and then filtered nodes with open REST API.
> Also @greenhat, is https://ergo.nodespyder.io/ the project you saw on discord?

Yes, I asked them originally, but I'm not recalling I got the answer.




Fix patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index c7edeb672..4978fb853 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -141,6 +141,11 @@ jobs:
           curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh -s -- -f
           curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash -
           sudo apt-get -y install nodejs
+      
+      - name: run `reqwest-wrap` WASM tests
+        run: |
+          cd vendored-deps/reqwest-wrap
+          wasm-pack test --chrome  --headless
 
       - name: run Wasm tests
         run: |
@@ -158,13 +163,6 @@ jobs:
           npm install
           npm run test
       
-      - name: run node JS integration tests
-        run: |
-          cd bindings/ergo-lib-wasm
-          # wasm-pack build # --features rest
-          npm install
-          npm run integration-test
-      
       - name: run browser JS tests
         run: |
           cd bindings/ergo-lib-wasm
@@ -252,7 +250,7 @@ jobs:
 
       - uses: actions-rs/toolchain@v1
         with:
-          toolchain: nightly-2021-11-10
+          toolchain: nightly-2022-03-29
           override: true
 
       - name: install deps
diff --git a/Cargo.toml b/Cargo.toml
index bdba531b7..472427309 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -18,6 +18,8 @@ members = [
     "bindings/ergo-lib-c-core",
     "bindings/ergo-lib-c",
     "bindings/ergo-lib-jni",
+    "vendored-deps/wasm-timer",
+    "vendored-deps/reqwest-wrap"
 ]
 
 [profile.release]
diff --git a/bindings/ergo-lib-c-core/Cargo.toml b/bindings/ergo-lib-c-core/Cargo.toml
index a70d362be..708485c84 100644
--- a/bindings/ergo-lib-c-core/Cargo.toml
+++ b/bindings/ergo-lib-c-core/Cargo.toml
@@ -19,13 +19,15 @@ serde_json = "1.0"
 thiserror = "1"
 tokio = { version = "1", features = ["full"], optional = true }
 futures-util = "0.3"
+url = "2.2"
+bounded-integer = { version = "^0.5", features = ["types"] }
 
 [dependencies.serde_with]
 version = "1.9.1"
 features = [ "json" ]
 
 [dependencies.bounded-vec]
-version = "^0.5.0"
+version = "^0.6.0"
 features = ["serde"]
 
 [features]
diff --git a/bindings/ergo-lib-c-core/src/error.rs b/bindings/ergo-lib-c-core/src/error.rs
index 6c0f5d9c6..f35e496a9 100644
--- a/bindings/ergo-lib-c-core/src/error.rs
+++ b/bindings/ergo-lib-c-core/src/error.rs
@@ -1,5 +1,5 @@
 #[cfg(feature = "rest")]
-use ergo_lib::ergo_rest::NodeError;
+use ergo_lib::ergo_rest::{NodeError, PeerDiscoveryError};
 
 use std::error;
 use thiserror::Error;
@@ -38,3 +38,10 @@ impl From<NodeError> for Error {
         Error::Misc(format!("{:?}", error).into())
     }
 }
+
+#[cfg(feature = "rest")]
+impl From<PeerDiscoveryError> for Error {
+    fn from(error: PeerDiscoveryError) -> Self {
+        Error::Misc(format!("{:?}", error).into())
+    }
+}
diff --git a/bindings/ergo-lib-c-core/src/nipopow.rs b/bindings/ergo-lib-c-core/src/nipopow.rs
index ce5f00ab3..94efd7364 100644
--- a/bindings/ergo-lib-c-core/src/nipopow.rs
+++ b/bindings/ergo-lib-c-core/src/nipopow.rs
@@ -6,13 +6,17 @@ use crate::{
     util::{const_ptr_as_ref, mut_ptr_as_mut},
     Error,
 };
+use derive_more::{From, Into};
 
 /// A structure representing NiPoPow proof.
-#[derive(Debug)]
-pub struct NipopowProof(ergo_lib::ergo_nipopow::NipopowProof);
+#[derive(Debug, From, Into)]
+pub struct NipopowProof(pub(crate) ergo_lib::ergo_nipopow::NipopowProof);
 pub type NipopowProofPtr = *mut NipopowProof;
 pub type ConstNipopowProofPtr = *const NipopowProof;
 
+#[cfg(feature = "rest")]
+impl ergo_lib::ergo_rest::NodeResponse for NipopowProof {}
+
 #[derive(Debug, PartialEq, Eq)]
 pub struct PoPowHeader(ergo_lib::ergo_nipopow::PoPowHeader);
 pub type PoPowHeaderPtr = *mut PoPowHeader;
diff --git a/bindings/ergo-lib-c-core/src/rest.rs b/bindings/ergo-lib-c-core/src/rest.rs
index 5c6627f8d..7470c7e56 100644
--- a/bindings/ergo-lib-c-core/src/rest.rs
+++ b/bindings/ergo-lib-c-core/src/rest.rs
@@ -1,3 +1,4 @@
 pub mod api;
+pub mod c_string_collection;
 pub mod node_conf;
 pub mod node_info;
diff --git a/bindings/ergo-lib-c-core/src/rest/api/node.rs b/bindings/ergo-lib-c-core/src/rest/api/node.rs
index 2b175f600..6ffbe207e 100644
--- a/bindings/ergo-lib-c-core/src/rest/api/node.rs
+++ b/bindings/ergo-lib-c-core/src/rest/api/node.rs
@@ -1,5 +1,11 @@
 //! Async REST API for Ergo node
 
+use std::ffi::{CStr, CString};
+use std::os::raw::c_char;
+use std::str::FromStr;
+
+use crate::block_header::ConstBlockIdPtr;
+use crate::rest::c_string_collection::{CStringCollection, CStringCollectionInner};
 use crate::rest::node_conf::NodeConfPtr;
 use crate::util::const_ptr_as_ref;
 use crate::Error;
@@ -14,6 +20,7 @@ use super::runtime::RestApiRuntimePtr;
 
 mod abortable;
 
+/// GET on /info endpoint
 pub unsafe fn rest_api_node_get_info(
     runtime_ptr: RestApiRuntimePtr,
     node_conf_ptr: NodeConfPtr,
@@ -33,3 +40,106 @@ pub unsafe fn rest_api_node_get_info(
     *request_handle_out = Box::into_raw(Box::new(request_handle));
     Ok(())
 }
+
+/// GET on /nipopow/proof/{minChainLength}/{suffixLength}/{headerId} endpoint
+pub unsafe fn rest_api_node_get_nipopow_proof_by_header_id(
+    runtime_ptr: RestApiRuntimePtr,
+    node_conf_ptr: NodeConfPtr,
+    callback: CompletionCallback,
+    request_handle_out: *mut RequestHandlePtr,
+    min_chain_length: u32,
+    suffix_len: u32,
+    header_id_ptr: ConstBlockIdPtr,
+) -> Result<(), Error> {
+    let runtime = const_ptr_as_ref(runtime_ptr, "runtime_ptr")?;
+    let node_conf = const_ptr_as_ref(node_conf_ptr, "node_conf_ptr")?.0;
+    let header_id = const_ptr_as_ref(header_id_ptr, "header_id_ptr")?.0.clone();
+    let abort_callback: AbortCallback = (&callback).into();
+    let abort_handle = spawn_abortable(runtime, async move {
+        match ergo_lib::ergo_rest::api::node::get_nipopow_proof_by_header_id(
+            node_conf,
+            min_chain_length,
+            suffix_len,
+            header_id,
+        )
+        .await
+        {
+            Ok(proof) => callback.succeeded(crate::nipopow::NipopowProof(proof)),
+            Err(e) => callback.failed(e.into()),
+        }
+    })?;
+    let request_handle = RequestHandle::new(abort_handle, abort_callback);
+    *request_handle_out = Box::into_raw(Box::new(request_handle));
+    Ok(())
+}
+
+/// Given a list of seed nodes, recursively determine all known peer nodes.
+pub unsafe fn rest_api_node_peer_discovery(
+    runtime_ptr: RestApiRuntimePtr,
+    callback: CompletionCallback,
+    request_handle_out: *mut RequestHandlePtr,
+    seeds_ptr: *const *const c_char,
+    num_seeds: usize,
+    max_parallel_requests: u16,
+    timeout_sec: u32,
+) -> Result<(), Error> {
+    if seeds_ptr.is_null() {
+        return Err(Error::Misc("seeds_ptr is null".into()));
+    }
+    if num_seeds == 0 {
+        return Err(Error::Misc("num_seeds must be > 0".into()));
+    }
+    let mut seeds_vec = Vec::with_capacity(num_seeds);
+    for i in 0..num_seeds {
+        let ptr = *(seeds_ptr.add(i));
+        if !ptr.is_null() {
+            let str = CStr::from_ptr(ptr)
+                .to_str()
+                .map_err(|e| Error::Misc(format!("CStr::from_ptr: {:?}", e).into()))?;
+            seeds_vec.push(
+                url::Url::from_str(str)
+                    .map_err(|e| Error::Misc(format!("Url::from_str: {:?}", e).into()))?,
+            );
+        } else {
+            return Err(Error::Misc("seeds_ptr contains null element".into()));
+        }
+    }
+    #[allow(clippy::unwrap_used)]
+    let seeds_bounded_vec = bounded_vec::NonEmptyVec::from_vec(seeds_vec).unwrap();
+
+    let max_parallel_requests = bounded_integer::BoundedU16::new(max_parallel_requests)
+        .ok_or_else(|| Error::Misc("max_parallel_requests must be > 0".into()))?;
+    let runtime = const_ptr_as_ref(runtime_ptr, "runtime_ptr")?;
+    let abort_callback: AbortCallback = (&callback).into();
+    let abort_handle = spawn_abortable(runtime, async move {
+        match ergo_lib::ergo_rest::api::node::peer_discovery(
+            seeds_bounded_vec,
+            max_parallel_requests,
+            std::time::Duration::from_secs(timeout_sec as u64),
+        )
+        .await
+        {
+            Ok(peers) => {
+                let length = peers.len();
+                let mut out = vec![];
+                for peer in peers {
+                    out.push(CString::new(peer.as_str()).unwrap().into_raw());
+                }
+
+                // Very important to shrink, since we assume `out`s length equals its capacity when
+                // deallocating its memory.
+                out.shrink_to_fit();
+                let known_peers = CStringCollection(CStringCollectionInner {
+                    ptr: out.as_mut_ptr(),
+                    length,
+                });
+                std::mem::forget(out);
+                callback.succeeded(known_peers)
+            }
+            Err(e) => callback.failed(e.into()),
+        }
+    })?;
+    let request_handle = RequestHandle::new(abort_handle, abort_callback);
+    *request_handle_out = Box::into_raw(Box::new(request_handle));
+    Ok(())
+}
diff --git a/bindings/ergo-lib-c-core/src/rest/c_string_collection.rs b/bindings/ergo-lib-c-core/src/rest/c_string_collection.rs
new file mode 100644
index 000000000..75757763e
--- /dev/null
+++ b/bindings/ergo-lib-c-core/src/rest/c_string_collection.rs
@@ -0,0 +1,49 @@
+use crate::error::*;
+use crate::util::{const_ptr_as_ref, mut_ptr_as_mut};
+use std::ffi::CString;
+use std::os::raw::c_char;
+
+/// Wrapper around a C-array of strings.
+#[repr(C)]
+pub(crate) struct CStringCollectionInner {
+    /// Pointer to the first string of the array
+    pub ptr: *mut *mut c_char,
+    /// Number of strings in the array
+    pub length: usize,
+}
+
+#[derive(derive_more::From, derive_more::Into)]
+pub struct CStringCollection(pub(crate) CStringCollectionInner);
+pub type CStringCollectionPtr = *mut CStringCollection;
+
+impl ergo_lib::ergo_rest::NodeResponse for CStringCollection {}
+
+/// Returns underlying pointer of the array
+pub unsafe fn c_string_collection_get_ptr(
+    c_string_collection_ptr: CStringCollectionPtr,
+) -> Result<*const *const c_char, Error> {
+    let c_string_collection = const_ptr_as_ref(c_string_collection_ptr, "c_string_collection_ptr")?;
+    Ok(c_string_collection.0.ptr as *const *const c_char)
+}
+
+/// Returns the length of the underlying array
+pub unsafe fn c_string_collection_get_length(
+    c_string_collection_ptr: CStringCollectionPtr,
+) -> Result<usize, Error> {
+    let c_string_collection = const_ptr_as_ref(c_string_collection_ptr, "c_string_collection_ptr")?;
+    Ok(c_string_collection.0.length)
+}
+
+/// Deletes a rust allocated Vec of `CString`s. IMPORTANT ASSUMPTION: the vec's length
+/// is equal to its capacity.
+#[no_mangle]
+pub unsafe fn delete_c_string_collection(ptr: CStringCollectionPtr) {
+    let coll = mut_ptr_as_mut(ptr, "ptr").unwrap();
+    let len = coll.0.length;
+    let v = Vec::from_raw_parts(coll.0.ptr, len, len);
+
+    for elem in v {
+        let s = CString::from_raw(elem);
+        std::mem::drop(s);
+    }
+}
diff --git a/bindings/ergo-lib-c/README.md b/bindings/ergo-lib-c/README.md
index a95c1c8cf..cc32f2b24 100644
--- a/bindings/ergo-lib-c/README.md
+++ b/bindings/ergo-lib-c/README.md
@@ -5,16 +5,18 @@ Does not use Rust types in the API and is suitable for C FFI.
 
 ## Build instructions
 
-The following command builds the project:
+The following command builds the project with REST API features:
 
 ```
-cargo build -p ergo-lib-c
+cargo build -p ergo-lib-c --features rest
 ```
 
+To build without REST simply omit `--features rest`.
+
 The C header file is generated with [cbindgen](https://github.com/eqrion/cbindgen). Run the following
 command:
 ```
-cbindgen --config cbindgen.toml --crate ergo-lib-c --output h/ergo_wallet.h
+cbindgen --config cbindgen.toml --crate ergo-lib-c --output h/ergo_lib.h
 ```
 **Note that we require a nightly version of rustc just for this command.** This is because we use
 macros to generate some C-types and `cbindgen` cannot directly generate types through them. However
diff --git a/bindings/ergo-lib-c/src/rest.rs b/bindings/ergo-lib-c/src/rest.rs
index 5c6627f8d..7470c7e56 100644
--- a/bindings/ergo-lib-c/src/rest.rs
+++ b/bindings/ergo-lib-c/src/rest.rs
@@ -1,3 +1,4 @@
 pub mod api;
+pub mod c_string_collection;
 pub mod node_conf;
 pub mod node_info;
diff --git a/bindings/ergo-lib-c/src/rest/api/node.rs b/bindings/ergo-lib-c/src/rest/api/node.rs
index 85af06709..483536fc8 100644
--- a/bindings/ergo-lib-c/src/rest/api/node.rs
+++ b/bindings/ergo-lib-c/src/rest/api/node.rs
@@ -1,11 +1,17 @@
+use ergo_lib_c_core::block_header::ConstBlockIdPtr;
 use ergo_lib_c_core::rest::api::callback::CompletionCallback;
-use ergo_lib_c_core::rest::api::node::rest_api_node_get_info;
+use ergo_lib_c_core::rest::api::node::{
+    rest_api_node_get_info, rest_api_node_get_nipopow_proof_by_header_id,
+    rest_api_node_peer_discovery,
+};
 use ergo_lib_c_core::rest::api::request_handle::RequestHandlePtr;
 use ergo_lib_c_core::rest::api::runtime::RestApiRuntimePtr;
 use ergo_lib_c_core::rest::node_conf::NodeConfPtr;
 use ergo_lib_c_core::Error;
 use ergo_lib_c_core::ErrorPtr;
+use std::os::raw::c_char;
 
+/// GET on /info endpoint
 #[no_mangle]
 pub unsafe extern "C" fn ergo_lib_rest_api_node_get_info(
     runtime_ptr: RestApiRuntimePtr,
@@ -16,3 +22,49 @@ pub unsafe extern "C" fn ergo_lib_rest_api_node_get_info(
     let res = rest_api_node_get_info(runtime_ptr, node_conf_ptr, callback, request_handle_out);
     Error::c_api_from(res)
 }
+
+/// GET on /nipopow/proof/{minChainLength}/{suffixLength}/{headerId} endpoint
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_rest_api_node_get_nipopow_proof_by_header_id(
+    runtime_ptr: RestApiRuntimePtr,
+    node_conf_ptr: NodeConfPtr,
+    callback: CompletionCallback,
+    request_handle_out: *mut RequestHandlePtr,
+    min_chain_length: u32,
+    suffix_len: u32,
+    header_id_ptr: ConstBlockIdPtr,
+) -> ErrorPtr {
+    let res = rest_api_node_get_nipopow_proof_by_header_id(
+        runtime_ptr,
+        node_conf_ptr,
+        callback,
+        request_handle_out,
+        min_chain_length,
+        suffix_len,
+        header_id_ptr,
+    );
+    Error::c_api_from(res)
+}
+
+/// GET on /peer_discovery endpoint
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_rest_api_node_peer_discovery(
+    runtime_ptr: RestApiRuntimePtr,
+    callback: CompletionCallback,
+    request_handle_out: *mut RequestHandlePtr,
+    seeds_ptr: *const *const c_char,
+    num_seeds: usize,
+    max_parallel_requests: u16,
+    timeout_sec: u32,
+) -> ErrorPtr {
+    let res = rest_api_node_peer_discovery(
+        runtime_ptr,
+        callback,
+        request_handle_out,
+        seeds_ptr,
+        num_seeds,
+        max_parallel_requests,
+        timeout_sec,
+    );
+    Error::c_api_from(res)
+}
diff --git a/bindings/ergo-lib-c/src/rest/c_string_collection.rs b/bindings/ergo-lib-c/src/rest/c_string_collection.rs
new file mode 100644
index 000000000..4c4debe11
--- /dev/null
+++ b/bindings/ergo-lib-c/src/rest/c_string_collection.rs
@@ -0,0 +1,25 @@
+use std::os::raw::c_char;
+
+use ergo_lib_c_core::rest::c_string_collection::*;
+
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_c_string_collection_get_ptr(
+    c_string_collection_ptr: CStringCollectionPtr,
+) -> *const *const c_char {
+    #[allow(clippy::unwrap_used)]
+    c_string_collection_get_ptr(c_string_collection_ptr).unwrap()
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_c_string_collection_get_length(
+    c_string_collection_ptr: CStringCollectionPtr,
+) -> usize {
+    #[allow(clippy::unwrap_used)]
+    c_string_collection_get_length(c_string_collection_ptr).unwrap()
+}
+
+/// Drop `CStringCollection`
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_c_string_collection_delete(ptr: CStringCollectionPtr) {
+    delete_c_string_collection(ptr);
+}
diff --git a/bindings/ergo-lib-ios/Sources/ErgoLib/CStringCollection.swift b/bindings/ergo-lib-ios/Sources/ErgoLib/CStringCollection.swift
new file mode 100644
index 000000000..f836dd35f
--- /dev/null
+++ b/bindings/ergo-lib-ios/Sources/ErgoLib/CStringCollection.swift
@@ -0,0 +1,46 @@
+import Foundation
+import ErgoLibC
+
+class CStringCollection: FromRawPtr {
+    internal var pointer: CStringCollectionPtr
+
+    internal init(withRawPointer ptr: CStringCollectionPtr) {
+        self.pointer = ptr
+    }
+
+    static func fromRawPtr(ptr: UnsafeRawPointer) -> Self {
+        return CStringCollection(withRawPointer: OpaquePointer(ptr)) as! Self
+    }
+
+    func getPtr() -> UnsafePointer<UnsafePointer<CChar>?>! {
+        return ergo_lib_c_string_collection_get_ptr(self.pointer)
+    }
+    
+    func getLength() -> UInt {
+        return ergo_lib_c_string_collection_get_length(self.pointer)
+    }
+    
+    func toArray() -> [String] {
+        var ptr = self.getPtr()
+        var res: [String] = []
+        if self.getLength() == UInt(0) {
+            return res;
+        }
+        for _ in 0 ..< self.getLength() {
+            if let ptrInner = ptr {
+                if let s = ptrInner.pointee {
+                    let str = String(cString: s)
+                    res.append(str)
+                    ptr = ptr!.advanced(by: 1)
+                }
+            } else {
+                break
+            }
+        }
+        return res
+    }
+
+    deinit {
+        ergo_lib_c_string_collection_delete(self.pointer)
+    }
+}
diff --git a/bindings/ergo-lib-ios/Sources/ErgoLib/Nipopow.swift b/bindings/ergo-lib-ios/Sources/ErgoLib/Nipopow.swift
index 3c8bb9b0f..048652a40 100644
--- a/bindings/ergo-lib-ios/Sources/ErgoLib/Nipopow.swift
+++ b/bindings/ergo-lib-ios/Sources/ErgoLib/Nipopow.swift
@@ -3,7 +3,7 @@ import Foundation
 import ErgoLibC
 
 /// NiPoPow proof
-class NipopowProof {
+class NipopowProof: FromRawPtr {
     internal var pointer: NipopowProofPtr
     
     /// Parse ``NipopowProof`` from JSON
@@ -16,6 +16,14 @@ class NipopowProof {
         self.pointer = ptr!
     }
     
+    internal init(withRawPointer ptr: NipopowProofPtr) {
+        self.pointer = ptr
+    }
+    
+    static func fromRawPtr(ptr: UnsafeRawPointer) -> Self {
+        return NipopowProof(withRawPointer: OpaquePointer(ptr)) as! Self
+    }
+    
     /// Implementation of the ≥ algorithm from [`KMZ17`], see Algorithm 4
     ///
     /// [`KMZ17`]: https://fc20.ifca.ai/preproceedings/74.pdf
diff --git a/bindings/ergo-lib-ios/Sources/ErgoLib/RestNodeApi.swift b/bindings/ergo-lib-ios/Sources/ErgoLib/RestNodeApi.swift
index 84b4ca703..04db5c463 100644
--- a/bindings/ergo-lib-ios/Sources/ErgoLib/RestNodeApi.swift
+++ b/bindings/ergo-lib-ios/Sources/ErgoLib/RestNodeApi.swift
@@ -30,8 +30,7 @@ class RestNodeApi {
         try checkError(error)
         return RequestHandle(withRawPtr: requestHandlerPtr!)
     }
-
-
+    
     /// GET on /info endpoint (async)
     @available(macOS 10.15, iOS 13, *)
     func getInfoAsync(nodeConf: NodeConf) async throws -> NodeInfo {
@@ -43,6 +42,114 @@ class RestNodeApi {
             } catch {}
         }
     }
+    
+    /// GET on /nipopow/proof/{minChainLength}/{suffixLength}/{headerId} endpoint
+    func getNipopowProofByHeaderId(
+        nodeConf: NodeConf,
+        minChainLength: UInt32,
+        suffixLen: UInt32,
+        headerId: BlockId,
+        closure: @escaping (Result<NipopowProof, Error>) -> Void
+    ) throws -> RequestHandle {
+        
+        let completion = wrapClosure(closure)
+        var requestHandlerPtr: RequestHandlePtr?
+        let error = ergo_lib_rest_api_node_get_nipopow_proof_by_header_id(
+            self.pointer,
+            nodeConf.pointer,
+            completion,
+            &requestHandlerPtr,
+            minChainLength,
+            suffixLen,
+            headerId.pointer
+        )
+        try checkError(error)
+        return RequestHandle(withRawPtr: requestHandlerPtr!)
+    }
+    
+    /// GET on /info endpoint (async)
+    @available(macOS 10.15, iOS 13, *)
+    func getNipopowProofByHeaderIdAsync(
+        nodeConf: NodeConf,
+        minChainLength: UInt32,
+        suffixLen: UInt32,
+        headerId: BlockId
+    ) async throws -> NipopowProof {
+        try await withCheckedThrowingContinuation { continuation in
+            do {
+                let _ = try getNipopowProofByHeaderId(
+                    nodeConf: nodeConf,
+                    minChainLength: minChainLength,
+                    suffixLen: suffixLen,
+                    headerId: headerId
+                ) { result in
+                    continuation.resume(with: result)
+                }
+            } catch {
+            }
+        }
+    }
+
+    /// Given a list of seed nodes, search for peer nodes with an active REST API on port 9053.
+    ///  - `seeds` represents a list of ergo node URLs from which to start peer discovery.
+    ///  - `max_parallel_requests` represents the maximum number of HTTP requests that can be made in
+    ///    parallel
+    ///  - `timeout` represents the amount of time that is spent search for peers. Once the timeout
+    ///    value is reached, return with the vec of active peers that have been discovered up to that
+    ///    point in time.
+    func peerDiscovery(
+        seeds: [URL],
+        maxParallelReqs: UInt16,
+        timeoutSec: UInt32,
+        closure: @escaping (Result<CStringCollection, Error>) -> Void
+    ) throws -> RequestHandle {
+        let completion = wrapClosure(closure)
+        var requestHandlerPtr: RequestHandlePtr?
+        // Need to convert `seeds` into [String] then clone it in a C-compatible way. This allows
+        // us to send it across the FFI boundary.
+        var ptr = seeds.map{ UnsafePointer<CChar>(strdup($0.absoluteString)) }
+        let error = ergo_lib_rest_api_node_peer_discovery(
+            self.pointer,
+            completion,
+            &requestHandlerPtr,
+            &ptr,
+            UInt(seeds.count),
+            maxParallelReqs,
+            timeoutSec
+        )
+        
+        // Deallocate the memory pointed-to by `ptr`
+        for p in ptr { free(UnsafeMutablePointer(mutating: p)) }
+        
+        try checkError(error)
+        return RequestHandle(withRawPtr: requestHandlerPtr!)
+    }
+    
+    
+    /// Given a list of seed nodes, search for peer nodes with an active REST API on port 9053.
+    ///  - `seeds` represents a list of ergo node URLs from which to start peer discovery.
+    ///  - `max_parallel_requests` represents the maximum number of HTTP requests that can be made in
+    ///    parallel
+    ///  - `timeout` represents the amount of time that is spent search for peers. Once the timeout
+    ///    value is reached, return with the vec of active peers that have been discovered up to that
+    ///    point in time.
+    @available(macOS 10.15, iOS 13, *)
+    func peerDiscoveryAsync(
+        seeds: [URL],
+        maxParallelReqs: UInt16,
+        timeoutSec: UInt32
+    ) async throws -> [URL] {
+        try await withCheckedThrowingContinuation { continuation in
+            do {
+                let _ = try peerDiscovery(
+                    seeds: seeds, maxParallelReqs: maxParallelReqs, timeoutSec: timeoutSec
+                ) { (result: Result<CStringCollection, Error>) in
+                    let r = result.map { $0.toArray().compactMap {URL(string: $0)}}
+                    continuation.resume(with: r)
+                }
+            } catch {}
+        }
+    }
 
     deinit {
         ergo_lib_rest_api_runtime_delete(self.pointer)
diff --git a/bindings/ergo-lib-ios/Tests/ErgoLibTests/Common.swift b/bindings/ergo-lib-ios/Tests/ErgoLibTests/Common.swift
index 13476f3c9..4a8e8ffac 100644
--- a/bindings/ergo-lib-ios/Tests/ErgoLibTests/Common.swift
+++ b/bindings/ergo-lib-ios/Tests/ErgoLibTests/Common.swift
@@ -1,3 +1,5 @@
+import Foundation
+
 // The following two functions copied from: https://stackoverflow.com/a/49890939
 
 func toPairsOfChars(pairs: [String], string: String) -> [String] {
@@ -14,3 +16,22 @@ func base16StringToBytes(_ string: String) -> [UInt8]? {
     let pairs = toPairsOfChars(pairs: [], string: string)
     return pairs.map { UInt8($0, radix: 16)! }
 }
+
+func getSeeds() -> [URL] {
+    [
+        "http://213.239.193.208:9030",
+        "http://159.65.11.55:9030",
+        "http://165.227.26.175:9030",
+        "http://159.89.116.15:9030",
+        "http://136.244.110.145:9030",
+        "http://94.130.108.35:9030",
+        "http://51.75.147.1:9020",
+        "http://221.165.214.185:9030",
+        "http://51.81.185.231:9031",
+        "http://217.182.197.196:9030",
+        "http://62.171.190.193:9030",
+        "http://173.212.220.9:9030",
+        "http://176.9.65.58:9130",
+        "http://213.152.106.56:9030"
+    ].map { URL(string: $0)! }
+}
diff --git a/bindings/ergo-lib-ios/Tests/ErgoLibTests/RestNodeApiIntegrationTests.swift b/bindings/ergo-lib-ios/Tests/ErgoLibTests/RestNodeApiIntegrationTests.swift
new file mode 100644
index 000000000..233727074
--- /dev/null
+++ b/bindings/ergo-lib-ios/Tests/ErgoLibTests/RestNodeApiIntegrationTests.swift
@@ -0,0 +1,46 @@
+import XCTest
+@testable import ErgoLib
+@testable import ErgoLibC
+
+// These integration tests assume that a local ergo node instance is running and its REST API is
+// accessible @ 127.0.0.1:9053.
+final class RestNodeApiIntegrationTests: XCTestCase {
+    func testGetInfo() throws {
+        let expectation = self.expectation(description: "getInfo")
+        let nodeConf = try NodeConf(withAddrString: "127.0.0.1:9053")
+        let restNodeApi = try RestNodeApi()
+        let _ = try restNodeApi.getInfo(nodeConf: nodeConf,
+            closure: { (res: Result<NodeInfo, Error>) -> () in
+                switch res {
+                    case .success(let nodeInfo):
+                        XCTAssert(!nodeInfo.getName().isEmpty)
+                    case .failure(let error):
+                        XCTFail(error.localizedDescription)
+                }
+                expectation.fulfill()
+            })
+        waitForExpectations(timeout: 5, handler: nil)
+    }
+
+    func testGetInfoAbort() throws {
+        let nodeConf = try NodeConf(withAddrString: "127.0.0.1:9053")
+        let restNodeApi = try RestNodeApi()
+        let handle = try restNodeApi.getInfo(nodeConf: nodeConf,
+            closure: { (res: Result<NodeInfo, Error>) -> () in
+                XCTFail("this should not be called")
+            })
+        handle.abort()
+    }
+
+     func testGetInfoAsync() async throws {
+         let nodeConf = try NodeConf(withAddrString: "127.0.0.1:9053")
+         let restNodeApi = try RestNodeApi()
+         let nodeInfo = try await restNodeApi.getInfoAsync(nodeConf: nodeConf)
+         XCTAssert(!nodeInfo.getName().isEmpty)
+
+         // test of re-using of tokio runtime
+         let nodeInfoNew = try await restNodeApi.getInfoAsync(nodeConf: nodeConf)
+         XCTAssert(!nodeInfoNew.getName().isEmpty)
+     }
+}
+
diff --git a/bindings/ergo-lib-ios/Tests/ErgoLibTests/RestNodeApiTests.swift b/bindings/ergo-lib-ios/Tests/ErgoLibTests/RestNodeApiTests.swift
index 1a6353419..10b5a717a 100644
--- a/bindings/ergo-lib-ios/Tests/ErgoLibTests/RestNodeApiTests.swift
+++ b/bindings/ergo-lib-ios/Tests/ErgoLibTests/RestNodeApiTests.swift
@@ -1,49 +1,103 @@
-
 import XCTest
 @testable import ErgoLib
 @testable import ErgoLibC
 
 final class RestNodeApiTests: XCTestCase {
-
-    func testGetInfo() throws {
-        let expectation = self.expectation(description: "getInfo")
-        let nodeConf = try NodeConf(withAddrString: "127.0.0.1:9053")
+    func testGetNipopowProofByHeaderIdNonAsync() throws {
+        let expectation = self.expectation(description: "getNipopowByHeaderIdNonAsync")
+        let nodeConf = try NodeConf(withAddrString: "213.239.193.208:9053")
         let restNodeApi = try RestNodeApi()
-        let _ = try restNodeApi.getInfo(nodeConf: nodeConf,
-            closure: { (res: Result<NodeInfo, Error>) -> () in 
+        let blockHeaders = try HeaderTests.generateBlockHeadersFromJSON()
+        let _ = try restNodeApi.getNipopowProofByHeaderId(
+            nodeConf: nodeConf,
+            minChainLength: UInt32(3),
+            suffixLen: UInt32(2),
+            headerId: blockHeaders.get(index: UInt(0))!.getBlockId(),
+            closure: { (res: Result<NipopowProof, Error>) -> () in
                 switch res {
-                    case .success(let nodeInfo): 
-                        XCTAssert(!nodeInfo.getName().isEmpty)
-                    case .failure(let error): 
+                    case .success(_):
+                        break
+                    case .failure(let error):
                         XCTFail(error.localizedDescription)
                 }
                 expectation.fulfill()
             })
         waitForExpectations(timeout: 5, handler: nil)
     }
-
-    func testGetInfoAbort() throws {
-        let nodeConf = try NodeConf(withAddrString: "127.0.0.1:9053")
+    
+    func testGetNipopowProofByHeaderAbort() throws {
+        let nodeConf = try NodeConf(withAddrString: "213.239.193.208:9053")
         let restNodeApi = try RestNodeApi()
-        let handle = try restNodeApi.getInfo(nodeConf: nodeConf,
-            closure: { (res: Result<NodeInfo, Error>) -> () in 
+        let blockHeaders = try HeaderTests.generateBlockHeadersFromJSON()
+        let handle = try restNodeApi.getNipopowProofByHeaderId(
+            nodeConf: nodeConf,
+            minChainLength: UInt32(3),
+            suffixLen: UInt32(2),
+            headerId: blockHeaders.get(index: UInt(0))!.getBlockId(),
+            closure: { (res: Result<NipopowProof, Error>) -> () in
                 XCTFail("this should not be called")
             })
         handle.abort()
     }
-
-    // need macOS 12.0 on Github GA
-    func testGetInfoAsync() throws {
-        let nodeConf = try NodeConf(withAddrString: "127.0.0.1:9053")
+    
+    func testGetNipopowProofByHeaderIdAsync() async throws {
+        let nodeConf = try NodeConf(withAddrString: "213.239.193.208:9053")
+        let restNodeApi = try RestNodeApi()
+        let blockHeaders = try HeaderTests.generateBlockHeadersFromJSON()
+        let proof = try await restNodeApi.getNipopowProofByHeaderIdAsync(
+            nodeConf: nodeConf,
+            minChainLength: UInt32(3),
+            suffixLen: UInt32(2),
+            headerId: blockHeaders.get(index: UInt(0))!.getBlockId()
+        )
+        XCTAssertNoThrow(try proof.toJSON()!)
+        
+        // test of re-using of tokio runtime
+        let proofNew = try await restNodeApi.getNipopowProofByHeaderIdAsync(
+            nodeConf: nodeConf,
+            minChainLength: UInt32(3),
+            suffixLen: UInt32(2),
+            headerId: blockHeaders.get(index: UInt(0))!.getBlockId()
+        )
+        XCTAssertNoThrow(try proofNew.toJSON()!)
+    }
+    
+    func testPeerDiscoveryNonAsync() throws {
+        let expectation = self.expectation(description: "peerDiscovery")
+        let restNodeApi = try RestNodeApi()
+        let _ = try restNodeApi.peerDiscovery(
+            seeds: getSeeds(),
+            maxParallelReqs: UInt16(30),
+            timeoutSec: UInt32(3),
+            closure: { (res: Result<CStringCollection, Error>) -> () in
+                switch res {
+                    case .success(let peers):
+                        XCTAssert(peers.getLength() > 0)
+                        break
+                    case .failure(let error):
+                        XCTFail(error.localizedDescription)
+                }
+                expectation.fulfill()
+            })
+        waitForExpectations(timeout: 60, handler: nil)
+    }
+    
+    func testPeerDiscoveryAsync() async throws {
         let restNodeApi = try RestNodeApi()
-        XCTAssertNoThrow(Task(priority: .medium) {
-            let nodeInfo = try await restNodeApi.getInfoAsync(nodeConf: nodeConf)
-            XCTAssert(!nodeInfo.getName().isEmpty)
-        })
+        let peers = try await restNodeApi.peerDiscoveryAsync(
+            seeds: getSeeds(),
+            maxParallelReqs: UInt16(30),
+            timeoutSec: UInt32(3)
+        )
+
+        XCTAssert(!peers.isEmpty)
+        
         // test of re-using of tokio runtime
-        XCTAssertNoThrow(Task(priority: .medium) {
-            let nodeInfo = try await restNodeApi.getInfoAsync(nodeConf: nodeConf)
-            XCTAssert(!nodeInfo.getName().isEmpty)
-        }) 
-    } 
+        let peersNew = try await restNodeApi.peerDiscoveryAsync(
+            seeds: getSeeds(),
+            maxParallelReqs: UInt16(30),
+            timeoutSec: UInt32(3)
+        )
+        XCTAssert(!peersNew.isEmpty)
+    }
 }
diff --git a/bindings/ergo-lib-jni/Cargo.toml b/bindings/ergo-lib-jni/Cargo.toml
index 39d2375bc..eb3b03afc 100644
--- a/bindings/ergo-lib-jni/Cargo.toml
+++ b/bindings/ergo-lib-jni/Cargo.toml
@@ -14,6 +14,6 @@ path = "src/main/rust/lib.rs"
 
 [dependencies]
 jni = "0.19.0"
-log = "0.4"
+log = "0.4.16"
 ergo-lib = {version = "^0.15.0", path = "../../ergo-lib"}
 ergo-lib-c-core = {version = "^0.15.0", path = "../ergo-lib-c-core"}
diff --git a/bindings/ergo-lib-wasm/Cargo.toml b/bindings/ergo-lib-wasm/Cargo.toml
index 35a506772..131343c0e 100644
--- a/bindings/ergo-lib-wasm/Cargo.toml
+++ b/bindings/ergo-lib-wasm/Cargo.toml
@@ -20,6 +20,9 @@ serde = { version = "1.0", features = ["derive"]}
 ergo-lib = { version = "^0.15.0", path = "../../ergo-lib"}
 serde_json = "1.0"
 js-sys = "0.3"
+web-sys = {version = "0.3", features = ["Url"]}
+url = "2.2"
+bounded-integer = { version = "^0.5", features = ["types"] }
 
 # used in elliptic-curve(in ergo-lib), compiled here with WASM support
 getrandom = {version = "0.2.3", features = ["js"]}
@@ -43,7 +46,7 @@ features = ["serde-serialize"]
 version = "0.4.28"
 
 [dependencies.bounded-vec]
-version = "^0.5.0"
+version = "^0.6.0"
 features = ["serde"]
 
 [dev-dependencies]
diff --git a/bindings/ergo-lib-wasm/karma.conf.js b/bindings/ergo-lib-wasm/karma.conf.js
index 58b59c3f1..d98028c9e 100644
--- a/bindings/ergo-lib-wasm/karma.conf.js
+++ b/bindings/ergo-lib-wasm/karma.conf.js
@@ -9,6 +9,11 @@ module.exports = function (config) {
       "tests_browser/**/*.js",
       { pattern: `${outputDir}/*.wasm`, included: false, served: true },
     ],
+    client: {
+      mocha: {
+        timeout: 60000
+      }
+    },
     webpack: {
       mode: "development",
       module: {
diff --git a/bindings/ergo-lib-wasm/package.json b/bindings/ergo-lib-wasm/package.json
index a65ad6232..4122b87d1 100644
--- a/bindings/ergo-lib-wasm/package.json
+++ b/bindings/ergo-lib-wasm/package.json
@@ -6,13 +6,12 @@
         "build": "webpack",
         "serve": "wasm-pack build --target browser && webpack-dev-server",
         "test": "wasm-pack build --target nodejs && mocha --require @babel/register --require @babel/polyfill 'tests/test*.js'",
-        "integration-test": "wasm-pack build --target nodejs --features rest && mocha --require @babel/register --require @babel/polyfill 'tests/integration_test*.js'",
         "test-browser": "rm -rf ./pkg && npm run build-browser && mv ./pkg-browser ./pkg && ./node_modules/.bin/karma start",
         "doc": "jsdoc -c jsdoc.json pkg/ergo_wallet_wasm.js README.md -d docs",
         "build-nodejs": "rm -rf ./pkg-nodejs && wasm-pack build --target nodejs --out-dir pkg-nodejs && cd pkg-nodejs && node ../scripts/publish_helper -nodejs",
         "build-nodejs-alpha": "rm -rf ./pkg-nodejs && wasm-pack build --target nodejs --out-dir pkg-nodejs && cd pkg-nodejs && node ../scripts/publish_helper -nodejs && npm version minor && node ../scripts/set_alpha_version -nodejs $(git rev-parse --short HEAD)",
-        "build-browser": "rm -rf ./pkg-browser && wasm-pack build --target browser --out-dir pkg-browser && cd pkg-browser && node ../scripts/publish_helper -browser",
-        "build-browser-alpha": "rm -rf ./pkg-browser && wasm-pack build --target browser --out-dir pkg-browser && cd pkg-browser && node ../scripts/publish_helper -browser && npm version minor && node ../scripts/set_alpha_version -browser $(git rev-parse --short HEAD)",
+        "build-browser": "rm -rf ./pkg-browser && wasm-pack build --target browser --out-dir pkg-browser --features rest && cd pkg-browser && node ../scripts/publish_helper -browser",
+        "build-browser-alpha": "rm -rf ./pkg-browser && wasm-pack build --target browser --out-dir pkg-browser --features rest && cd pkg-browser && node ../scripts/publish_helper -browser && npm version minor && node ../scripts/set_alpha_version -browser $(git rev-parse --short HEAD)",
         "publish-nodejs": "npm run build-nodejs &&  cd pkg-nodejs && npm publish",
         "publish-browser": "npm run build-browser && cd pkg-browser && npm publish"
     },
diff --git a/bindings/ergo-lib-wasm/src/error_conversion.rs b/bindings/ergo-lib-wasm/src/error_conversion.rs
index 23b8db207..4d59dd368 100644
--- a/bindings/ergo-lib-wasm/src/error_conversion.rs
+++ b/bindings/ergo-lib-wasm/src/error_conversion.rs
@@ -9,7 +9,7 @@ use bounded_vec::BoundedVecOutOfBounds;
 use ergo_lib::ergo_chain_types::DigestNError;
 use ergo_lib::ergo_nipopow::NipopowProofError;
 #[cfg(feature = "rest")]
-use ergo_lib::ergo_rest::NodeError;
+use ergo_lib::ergo_rest::{NodeError, PeerDiscoveryError};
 use ergo_lib::ergotree_interpreter::sigma_protocol::verifier::VerifierError;
 use ergo_lib::ergotree_ir::chain::address::AddressEncoderError;
 use ergo_lib::ergotree_ir::chain::address::AddressError;
@@ -30,6 +30,8 @@ use ergo_lib::{
     wallet::{box_selector::BoxSelectorError, tx_builder::TxBuilderError, WalletError},
 };
 use serde_json::error::Error;
+#[cfg(feature = "rest")]
+use url::ParseError;
 use wasm_bindgen::JsValue;
 
 /// Ideally we'd like to implement `From<E> for JsValue` for a range of different `ergo-lib` error
@@ -93,3 +95,7 @@ from_error_to_wrap_via_debug!(NipopowProofError);
 from_error_to_wrap_via_debug!(VerifierError);
 #[cfg(feature = "rest")]
 from_error_to_wrap_via_debug!(NodeError);
+#[cfg(feature = "rest")]
+from_error_to_wrap_via_debug!(ParseError);
+#[cfg(feature = "rest")]
+from_error_to_wrap_via_debug!(PeerDiscoveryError);
diff --git a/bindings/ergo-lib-wasm/src/rest/api.rs b/bindings/ergo-lib-wasm/src/rest/api.rs
index e2854a228..b6e491481 100644
--- a/bindings/ergo-lib-wasm/src/rest/api.rs
+++ b/bindings/ergo-lib-wasm/src/rest/api.rs
@@ -3,7 +3,9 @@
 use wasm_bindgen::prelude::*;
 
 use super::node_conf::NodeConf;
-use crate::error_conversion::to_js;
+use crate::{block_header::BlockId, error_conversion::to_js, nipopow::NipopowProof};
+use bounded_vec::NonEmptyVec;
+use std::time::Duration;
 
 #[wasm_bindgen]
 /// GET on /info endpoint
@@ -14,3 +16,75 @@ pub async fn get_info(node: NodeConf) -> Result<JsValue, JsValue> {
         .map_err(to_js)
         .map(|info| JsValue::from_str(&info.name))
 }
+
+#[wasm_bindgen]
+/// GET on /nipopow/proof/{minChainLength}/{suffixLength}/{headerId} endpoint
+pub async fn get_nipopow_proof_by_header_id(
+    node: NodeConf,
+    min_chain_length: u32,
+    suffix_len: u32,
+    header_id: BlockId,
+) -> Result<NipopowProof, JsValue> {
+    ergo_lib::ergo_rest::api::node::get_nipopow_proof_by_header_id(
+        node.into(),
+        min_chain_length,
+        suffix_len,
+        header_id.into(),
+    )
+    .await
+    .map_err(to_js)
+    .map(NipopowProof::from)
+}
+
+/// List of peer urls returned from `peer_discovery`. We need this wrapper struct because the
+/// `wasm_bindgen` macro currently cannot deal with `Result<Box<[T]>, JsValue>`, for any value `T`
+/// that can be converted into a `JsValue` (`Result<Box<[web_sys::Url]>, JsValue>` would be a
+/// convenient return type for `peer_discovery`).
+#[wasm_bindgen]
+pub struct PeerUrls(Vec<web_sys::Url>);
+
+#[wasm_bindgen]
+impl PeerUrls {
+    /// Returns the number of elements in the collection
+    pub fn len(&self) -> usize {
+        self.0.len()
+    }
+
+    /// Returns the element of the collection with a given index
+    pub fn get(&self, index: usize) -> web_sys::Url {
+        self.0[index].clone()
+    }
+}
+
+/// Given a list of seed nodes, search for peer nodes with an active REST API on port 9053.
+///  - `seeds` represents a list of ergo node URLs from which to start peer discovery.
+///  - `max_parallel_requests` represents the maximum number of HTTP requests that can be made in
+///    parallel
+///  - `timeout` represents the amount of time that is spent search for peers. Once the timeout
+///    value is reached, return with the vec of active peers that have been discovered up to that
+///    point in time.
+#[wasm_bindgen]
+pub async fn peer_discovery(
+    seeds: Box<[web_sys::Url]>,
+    max_parallel_requests: u16,
+    timeout_sec: u32,
+) -> Result<PeerUrls, JsValue> {
+    let mut converted_seeds = vec![];
+    for seed in &*seeds {
+        let str: String = seed.to_string().into();
+        converted_seeds.push(url::Url::parse(&str).map_err(to_js)?);
+    }
+    let seeds = NonEmptyVec::from_vec(converted_seeds).map_err(to_js)?;
+    let n = u16::max(max_parallel_requests, 1);
+    #[allow(clippy::unwrap_used)]
+    let max_parallel_requests = bounded_integer::BoundedU16::new(n).unwrap();
+    let timeout = Duration::from_secs(timeout_sec as u64);
+    let res = ergo_lib::ergo_rest::api::node::peer_discovery(seeds, max_parallel_requests, timeout)
+        .await
+        .map_err(to_js)?;
+    let mut peer_urls = vec![];
+    for url in res {
+        peer_urls.push(web_sys::Url::new(url.as_str())?);
+    }
+    Ok(PeerUrls(peer_urls))
+}
diff --git a/ergo-chain-types/src/peer_addr.rs b/ergo-chain-types/src/peer_addr.rs
index bf42406da..497d34026 100644
--- a/ergo-chain-types/src/peer_addr.rs
+++ b/ergo-chain-types/src/peer_addr.rs
@@ -7,12 +7,15 @@ use std::{
 
 use derive_more::FromStr;
 use derive_more::{Display, From, Into};
+use serde::{Deserialize, Serialize};
 use sigma_ser::{ScorexSerializable, ScorexSerializationError};
 use url::Url;
 
 /// Peer address
-#[derive(PartialEq, Eq, Debug, Copy, Clone, From, Into, Hash, Display, FromStr)]
-pub struct PeerAddr(SocketAddr);
+#[derive(
+    PartialEq, Eq, Debug, Copy, Clone, From, Into, Hash, Display, FromStr, Deserialize, Serialize,
+)]
+pub struct PeerAddr(pub SocketAddr);
 
 impl PeerAddr {
     /// Size in bytes of the ip address associated with this peer address
diff --git a/ergo-chain-types/src/peer_connection_dir.rs b/ergo-chain-types/src/peer_connection_dir.rs
index 1db990eb6..5ec2b23fe 100644
--- a/ergo-chain-types/src/peer_connection_dir.rs
+++ b/ergo-chain-types/src/peer_connection_dir.rs
@@ -1,5 +1,7 @@
+use serde::{Deserialize, Serialize};
+
 /// Direction of the connection to a peer
-#[derive(PartialEq, Eq, Debug, Copy, Clone, Hash)]
+#[derive(PartialEq, Eq, Debug, Copy, Clone, Hash, Deserialize, Serialize)]
 pub enum ConnectionDirection {
     /// A peer is connecting to us
     Incoming,
diff --git a/ergo-lib/Cargo.toml b/ergo-lib/Cargo.toml
index 4ad14018e..15553f5bf 100644
--- a/ergo-lib/Cargo.toml
+++ b/ergo-lib/Cargo.toml
@@ -30,8 +30,7 @@ serde = { version = "1.0", features = ["derive"], optional = true }
 serde_json = { version = "1.0", optional = true }
 thiserror = "1"
 derive_more = "0.99"
-# bounded-vec = { git = "https://github.com/ergoplatform/bounded-vec", rev="f684d78" }
-bounded-vec = { version = "^0.5.0" }
+bounded-vec = { version = "^0.6.0" }
 num-bigint = { version = "0.4.0", features = ["serde"] }
 proptest-derive = {version = "0.3.0", optional = true }
 sha2 = { version = "0.9.8" }
diff --git a/ergo-p2p/Cargo.toml b/ergo-p2p/Cargo.toml
index 148ec2882..b022af2c5 100644
--- a/ergo-p2p/Cargo.toml
+++ b/ergo-p2p/Cargo.toml
@@ -19,7 +19,7 @@ sigma-util = { version = "^0.4.0", path = "../sigma-util" }
 ergo-chain-types = { version = "^0.2.0", path = "../ergo-chain-types"}
 thiserror = "1"
 derive_more = "0.99"
-bounded-vec = { version = "^0.5.0" }
+bounded-vec = { version = "^0.6.0" }
 proptest-derive = {version = "0.3.0", optional = true }
 
 futures = "0.3"
diff --git a/ergo-rest/Cargo.toml b/ergo-rest/Cargo.toml
index fe7d7676e..221bdf9e4 100644
--- a/ergo-rest/Cargo.toml
+++ b/ergo-rest/Cargo.toml
@@ -14,19 +14,29 @@ exclude = [
 crate-type = ["cdylib", "rlib"]
 
 [dependencies]
+async-trait = "^0.1"
+bounded-integer = { version = "^0.5", features = ["types"] }
+bounded-vec = { version = "^0.6.0", features=["serde"] }
 sigma-ser = { version = "^0.4.0", path = "../sigma-ser" }
 sigma-util = { version = "^0.4.0", path = "../sigma-util" }
 ergo-chain-types = { version = "^0.2.0", path = "../ergo-chain-types" }
 ergo-nipopow = { version = "^0.2.0", path = "../ergo-nipopow" }
+futures = "0.3"
 thiserror = "1"
 derive_more = "0.99"
 proptest-derive = {version = "0.3.0", optional = true }
-reqwest = { version = "0.11.7", default-features = false, features = ["json"] }
+reqwest = { package = "reqwest-wrap", version = "0.11.10", path = "../vendored-deps/reqwest-wrap", features = ["json"] }
 serde = { version = "1.0", features = ["derive"]}
 serde_json = { version = "1.0", optional = true }
+url = "2.2"
+
+[target.'cfg(target_arch = "wasm32")'.dependencies]
+wasm-bindgen-futures = "0.4"
+wasm-timer = { version = "0.2.5", path = "../vendored-deps/wasm-timer" } 
 
 [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
 tokio = { version = "1", features = ["full"] }
+tokio-stream = "0.1"
 
 [dependencies.proptest]
 # wasm support, via https://altsysrq.github.io/proptest-book/proptest/wasm.html
@@ -39,6 +49,7 @@ features = ["std"]
 optional = true
 
 [features]
+json = ["serde_json"]
 arbitrary = ["proptest", "proptest-derive"]
 
 [dev-dependencies]
diff --git a/ergo-rest/src/api.rs b/ergo-rest/src/api.rs
index a400b7349..5452e3afc 100644
--- a/ergo-rest/src/api.rs
+++ b/ergo-rest/src/api.rs
@@ -1,3 +1,23 @@
 //! REST API for the services in Ergo ecosystem (node, explorer, etc.)
 
+use reqwest::{header::CONTENT_TYPE, Client, RequestBuilder};
+
+use crate::NodeConf;
+
 pub mod node;
+mod peer_discovery_internals;
+
+fn set_req_headers(rb: RequestBuilder, node: NodeConf) -> RequestBuilder {
+    rb.header("accept", "application/json")
+        .header("api_key", node.get_node_api_header())
+        .header(CONTENT_TYPE, "application/json")
+}
+
+fn build_client(node_conf: &NodeConf) -> Result<Client, reqwest::Error> {
+    let builder = reqwest::Client::builder();
+    if let Some(t) = node_conf.timeout {
+        builder.timeout(t).build()
+    } else {
+        builder.build()
+    }
+}
diff --git a/ergo-rest/src/api/node.rs b/ergo-rest/src/api/node.rs
index 37c5d2fdf..dda684764 100644
--- a/ergo-rest/src/api/node.rs
+++ b/ergo-rest/src/api/node.rs
@@ -1,36 +1,19 @@
 //! Ergo node REST API endpoints
 
+use bounded_integer::BoundedU16;
+use bounded_vec::NonEmptyVec;
 use ergo_chain_types::BlockId;
 use ergo_nipopow::NipopowProof;
-use reqwest::header::CONTENT_TYPE;
-use reqwest::Client;
-use reqwest::RequestBuilder;
+use std::time::Duration;
+use url::Url;
 
+use crate::error::PeerDiscoveryError;
 use crate::NodeConf;
 use crate::NodeError;
 use crate::NodeInfo;
-use crate::PeerInfo;
 
-fn set_req_headers(rb: RequestBuilder, node: NodeConf) -> RequestBuilder {
-    rb.header("accept", "application/json")
-        .header("api_key", node.get_node_api_header())
-        .header(CONTENT_TYPE, "application/json")
-}
-
-fn build_client(_node_conf: &NodeConf) -> Result<Client, reqwest::Error> {
-    #[cfg(not(target_arch = "wasm32"))]
-    let builder = if let Some(timeout) = _node_conf.timeout {
-        reqwest::Client::builder().timeout(timeout)
-    } else {
-        reqwest::Client::builder()
-    };
-    // there is no `timeout` method yet in Wasm reqwest implementation
-    // see https://github.com/seanmonstar/reqwest/issues/1135
-    #[cfg(target_arch = "wasm32")]
-    let builder = reqwest::Client::builder();
-
-    builder.build()
-}
+use super::build_client;
+use super::set_req_headers;
 
 /// GET on /info endpoint
 pub async fn get_info(node: NodeConf) -> Result<NodeInfo, NodeError> {
@@ -45,19 +28,48 @@ pub async fn get_info(node: NodeConf) -> Result<NodeInfo, NodeError> {
         .await?)
 }
 
-/// GET on /peers/all endpoint
-pub async fn get_peers_all(_node: NodeConf) -> Result<Vec<PeerInfo>, NodeError> {
-    todo!()
+/// Given a list of seed nodes, search for peer nodes with an active REST API on port 9053.
+///  - `seeds` represents a list of ergo node URLs from which to start peer discovery.
+///  - `max_parallel_requests` represents the maximum number of HTTP requests that can be made in
+///    parallel
+///  - `timeout` represents the amount of time that is spent search for peers. Once the timeout
+///    value is reached, return with the vec of active peers that have been discovered up to that
+///    point in time.
+pub async fn peer_discovery(
+    seeds: NonEmptyVec<Url>,
+    max_parallel_requests: BoundedU16<1, { u16::MAX }>,
+    timeout: Duration,
+) -> Result<Vec<Url>, PeerDiscoveryError> {
+    super::peer_discovery_internals::peer_discovery_inner(seeds, max_parallel_requests, timeout)
+        .await
 }
 
 /// GET on /nipopow/proof/{minChainLength}/{suffixLength}/{headerId} endpoint
 pub async fn get_nipopow_proof_by_header_id(
-    _node: NodeConf,
-    _min_chain_length: u32,
-    _suffix_len: u32,
-    _header_id: BlockId,
+    node: NodeConf,
+    min_chain_length: u32,
+    suffix_len: u32,
+    header_id: BlockId,
 ) -> Result<NipopowProof, NodeError> {
-    todo!()
+    if min_chain_length == 0 || suffix_len == 0 {
+        return Err(NodeError::InvalidNumericalUrlSegment);
+    }
+    let header_str = String::from(header_id.0);
+    let mut path = "nipopow/proof/".to_owned();
+    path.push_str(&*min_chain_length.to_string());
+    path.push('/');
+    path.push_str(&*suffix_len.to_string());
+    path.push('/');
+    path.push_str(&*header_str);
+    #[allow(clippy::unwrap_used)]
+    let url = node.addr.as_http_url().join(&*path).unwrap();
+    let client = build_client(&node)?;
+    let rb = client.get(url);
+    Ok(set_req_headers(rb, node)
+        .send()
+        .await?
+        .json::<NipopowProof>()
+        .await?)
 }
 
 // pub async fn get_blocks_header_id_proof_for_tx_id(
@@ -69,9 +81,9 @@ pub async fn get_nipopow_proof_by_header_id(
 // }
 
 #[allow(clippy::unwrap_used)]
-#[allow(unused_imports)]
 #[cfg(test)]
 mod tests {
+    use std::convert::TryFrom;
     use std::str::FromStr;
     use std::time::Duration;
 
@@ -81,7 +93,6 @@ mod tests {
 
     #[test]
     fn test_get_info() {
-        // let runtime_inner = tokio::runtime::Runtime::new().unwrap();
         let runtime_inner = tokio::runtime::Builder::new_multi_thread()
             .enable_all()
             .build()
@@ -94,4 +105,83 @@ mod tests {
         let res = runtime_inner.block_on(async { get_info(node_conf).await.unwrap() });
         assert_ne!(res.name, "");
     }
+
+    #[test]
+    fn test_get_nipopow_proof_by_header_id() {
+        use ergo_chain_types::{BlockId, Digest32};
+        let header_id = BlockId(
+            Digest32::try_from(String::from(
+                "9bcb535c2d05fbced6de3d73c63337d6deb64af387438fa748d66ddf3d33ee89",
+            ))
+            .unwrap(),
+        );
+        let runtime_inner = tokio::runtime::Builder::new_multi_thread()
+            .enable_all()
+            .build()
+            .unwrap();
+        let node_conf = NodeConf {
+            addr: PeerAddr::from_str("213.239.193.208:9053").unwrap(),
+            api_key: None,
+            timeout: Some(Duration::from_secs(5)),
+        };
+        let m = 3;
+        let k = 4;
+        let res = runtime_inner.block_on(async {
+            get_nipopow_proof_by_header_id(node_conf, m, k, header_id)
+                .await
+                .unwrap()
+        });
+        assert!(!res.prefix.is_empty());
+        assert_eq!(res.m, m);
+        assert_eq!(res.k, k);
+    }
+
+    #[test]
+    fn test_peer_discovery() {
+        let seeds: Vec<_> = [
+            "http://213.239.193.208:9030",
+            "http://159.65.11.55:9030",
+            "http://165.227.26.175:9030",
+            "http://159.89.116.15:9030",
+            "http://136.244.110.145:9030",
+            "http://94.130.108.35:9030",
+            "http://51.75.147.1:9020",
+            "http://221.165.214.185:9030",
+            "http://51.81.185.231:9031",
+            "http://217.182.197.196:9030",
+            "http://62.171.190.193:9030",
+            "http://173.212.220.9:9030",
+            "http://176.9.65.58:9130",
+            "http://213.152.106.56:9030",
+        ]
+        .iter()
+        .map(|s| Url::from_str(s).unwrap())
+        .collect();
+        let runtime_inner = tokio::runtime::Builder::new_multi_thread()
+            .enable_all()
+            .build()
+            .unwrap();
+        let (res_with_quick_timeout, res_with_longer_timeout) = runtime_inner.block_on(async {
+            let res_quick = peer_discovery(
+                NonEmptyVec::from_vec(seeds.clone()).unwrap(),
+                BoundedU16::new(5).unwrap(),
+                Duration::from_millis(2010),
+            )
+            .await
+            .unwrap();
+
+            let _ = tokio::time::sleep(Duration::from_secs(5)).await;
+
+            let res_long = peer_discovery(
+                NonEmptyVec::from_vec(seeds).unwrap(),
+                BoundedU16::new(5).unwrap(),
+                Duration::from_millis(10000),
+            )
+            .await
+            .unwrap();
+            (res_quick, res_long)
+        });
+        assert!(!res_with_longer_timeout.is_empty());
+        assert!(res_with_quick_timeout.len() <= res_with_longer_timeout.len());
+    }
 }
diff --git a/ergo-rest/src/api/peer_discovery_internals.rs b/ergo-rest/src/api/peer_discovery_internals.rs
new file mode 100644
index 000000000..e12f7d2c3
--- /dev/null
+++ b/ergo-rest/src/api/peer_discovery_internals.rs
@@ -0,0 +1,410 @@
+//! This module contains the implementation of `peer_discovery`.  It's structured as 2 separate
+//! tasks:
+//!
+//!  - Task 1 is responsible for tracking which nodes are active/inactive and making sure that any
+//!    given ergo node is queried exactly once.
+//!  - Task 2's job is to wait for a URL from task 1, make the actual HTTP requests to that URL, and
+//!    to report the result back to task 1.
+//! ```text
+//!                              <ergo node URL>
+//!               __________________________________________________
+//!              |                                                  |
+//!              |                                                  v
+//!  /----------------------\                   /----------------------\
+//!  | 1. Track node status |                   | 2. HTTP request task |
+//!  \----------------------/                   \----------------------/
+//!              ^                                                  |
+//!              |__________________________________________________|
+//!                <active node| non-active node| list of peers>   
+//! ```
+use crate::error::PeerDiscoveryError;
+use crate::{api::node::get_info, NodeConf, NodeError, PeerInfo};
+use async_trait::async_trait;
+use bounded_integer::BoundedU16;
+use bounded_vec::NonEmptyVec;
+use ergo_chain_types::PeerAddr;
+use std::fmt::Debug;
+use std::{collections::HashSet, time::Duration};
+use url::Url;
+
+use super::{build_client, set_req_headers};
+
+pub(crate) async fn peer_discovery_inner(
+    seeds: NonEmptyVec<Url>,
+    max_parallel_requests: BoundedU16<1, { u16::MAX }>,
+    timeout: Duration,
+) -> Result<Vec<Url>, PeerDiscoveryError> {
+    let buffer_size = usize::max(max_parallel_requests.get() as usize, seeds.len());
+    #[cfg(not(target_arch = "wasm32"))]
+    let (tx_msg, rx_msg) =
+        tokio::sync::mpsc::channel::<super::peer_discovery_internals::Msg>(buffer_size);
+    #[cfg(not(target_arch = "wasm32"))]
+    let (tx_url, rx_url) = tokio::sync::mpsc::channel::<Url>(buffer_size);
+    #[cfg(not(target_arch = "wasm32"))]
+    let url_stream = tokio_stream::wrappers::ReceiverStream::new(rx_url);
+    #[cfg(not(target_arch = "wasm32"))]
+    let msg_stream = tokio_stream::wrappers::ReceiverStream::new(rx_msg);
+
+    #[cfg(target_arch = "wasm32")]
+    let (tx_msg, rx_msg) = futures::channel::mpsc::channel::<Msg>(buffer_size);
+    #[cfg(target_arch = "wasm32")]
+    let (tx_url, rx_url) = futures::channel::mpsc::channel::<Url>(buffer_size);
+    #[cfg(target_arch = "wasm32")]
+    let url_stream = rx_url;
+    #[cfg(target_arch = "wasm32")]
+    let msg_stream = rx_msg;
+
+    super::peer_discovery_internals::peer_discovery_impl(
+        seeds,
+        max_parallel_requests,
+        tx_msg,
+        msg_stream,
+        tx_url,
+        url_stream,
+        timeout,
+    )
+    .await
+}
+
+/// Implementation of `peer_discovery`.
+async fn peer_discovery_impl<
+    SendMsg: 'static + ChannelInfallibleSender<Msg> + Clone + Send + Sync,
+    SendUrl: 'static + ChannelInfallibleSender<Url> + ChannelTrySender<Url> + Clone + Send + Sync,
+>(
+    seeds: NonEmptyVec<Url>,
+    max_parallel_requests: BoundedU16<1, { u16::MAX }>,
+    tx_msg: SendMsg,
+    msg_stream: impl futures::Stream<Item = Msg> + Send + 'static,
+    mut tx_url: SendUrl,
+    url_stream: impl futures::Stream<Item = Url> + Send + 'static,
+    timeout: Duration,
+) -> Result<Vec<Url>, PeerDiscoveryError> {
+    use futures::future::FutureExt;
+    use futures::StreamExt;
+
+    let mut seeds_set: HashSet<Url> = HashSet::new();
+
+    for mut seed_url in seeds {
+        #[allow(clippy::unwrap_used)]
+        seed_url.set_port(None).unwrap();
+        seeds_set.insert(seed_url);
+    }
+
+    // Task 2 from the schematic above
+    spawn_http_request_task(
+        tx_msg,
+        url_stream,
+        max_parallel_requests,
+        Duration::from_secs(2),
+    );
+
+    // Start with requests to seed nodes.
+    for url in &seeds_set {
+        tx_url.infallible_send(url.clone()).await;
+    }
+
+    // (*) This variable represents the number of URLs that need to be checked to see whether it
+    // corresponds to an active Ergo node. `count` is crucial to allow this function to terminate,
+    // as once it reaches zero we break the loop below. This leads us to drop `tx_url`, which is the
+    // sender side of the receiver stream `rx_url_stream`, allowing task 1 to end.
+    let mut count = seeds_set.len();
+
+    let mut visited_active_peers = HashSet::new();
+    let mut visited_peers = HashSet::new();
+
+    // Stack of peers to evaluate. Used as a growable buffer for when the (tx_url, rx_url) channel
+    // gets full.
+    let mut peer_stack: Vec<PeerInfo> = vec![];
+
+    // Here we spawn a task that triggers a signal after `timeout` has elapsed.
+    #[cfg(target_arch = "wasm32")]
+    let rx_timeout_signal = {
+        let (tx, rx) = futures::channel::oneshot::channel::<()>();
+        wasm_bindgen_futures::spawn_local(async move {
+            let _ = wasm_timer::Delay::new(timeout).await;
+            let _ = tx.send(());
+        });
+        rx.into_stream()
+    };
+
+    #[cfg(not(target_arch = "wasm32"))]
+    let rx_timeout_signal = {
+        let (tx, rx) = tokio::sync::oneshot::channel::<()>();
+        tokio::spawn(async move {
+            let _ = tokio::time::sleep(timeout).await;
+            let _ = tx.send(());
+        });
+        rx.into_stream()
+    };
+
+    // In addition to listening for `Msg`s from the HTTP request task, we need to watch for the
+    // timeout signal so we can exit early. The solution is to combine the streams.
+    enum C {
+        RxMsg(Msg),
+        RxTimeoutSignal,
+    }
+
+    type CombinedStream = std::pin::Pin<Box<dyn futures::stream::Stream<Item = C> + Send>>;
+
+    let streams: Vec<CombinedStream> = vec![
+        msg_stream.map(C::RxMsg).boxed(),
+        rx_timeout_signal.map(|_| C::RxTimeoutSignal).boxed(),
+    ];
+    let mut combined_stream = futures::stream::select_all(streams);
+
+    'loop_: while let Some(n) = combined_stream.next().await {
+        match n {
+            C::RxMsg(p) => {
+                // Try pushing as many peers as can be allowed in the (tx_url, rx_url) channel
+                while let Some(peer) = peer_stack.pop() {
+                    let mut url = peer.addr.as_http_url();
+                    #[allow(clippy::unwrap_used)]
+                    url.set_port(None).unwrap();
+                    if !visited_peers.contains(&url) {
+                        match tx_url.try_send(url.clone()) {
+                            Ok(_) => {
+                                visited_peers.insert(url);
+                                count += 1;
+                            }
+                            Err(TrySendError::Full) => {
+                                // Push it back on the stack, try again later.
+                                peer_stack.push(peer);
+                                break;
+                            }
+                            Err(TrySendError::Closed) => {
+                                return Err(PeerDiscoveryError::MpscSender);
+                            }
+                        }
+                    }
+                }
+                match p {
+                    Msg::AddActiveNode(mut url) => {
+                        #[allow(clippy::unwrap_used)]
+                        url.set_port(None).unwrap();
+                        //println!("Active node {}", url);
+                        visited_active_peers.insert(url.clone());
+                        visited_peers.insert(url);
+                        count -= 1;
+                        if count == 0 {
+                            break 'loop_;
+                        }
+                    }
+                    Msg::AddInactiveNode(mut url) => {
+                        #[allow(clippy::unwrap_used)]
+                        url.set_port(None).unwrap();
+                        visited_peers.insert(url);
+                        count -= 1;
+                        if count == 0 {
+                            break 'loop_;
+                        }
+                    }
+                    Msg::CheckPeers(peers) => {
+                        peer_stack.extend(peers);
+                    }
+                }
+            }
+            C::RxTimeoutSignal => {
+                break 'loop_;
+            }
+        }
+    }
+
+    //println!("Total # nodes visited: {}", visited_peers.len());
+
+    drop(tx_url);
+    let coll: Vec<_> = visited_active_peers
+        .difference(&seeds_set)
+        .into_iter()
+        .cloned()
+        .collect();
+    Ok(coll)
+}
+
+/// Given a stream that receives URLs of full ergo nodes, spawn a task (task 2 in the schematic
+/// above) which checks if it is active.  If so, request its peers. In all cases, a message (enum
+/// `Msg`) is sent out to notify the listener.
+fn spawn_http_request_task<
+    SendMsg: ChannelInfallibleSender<Msg> + Clone + Send + Sync + 'static,
+>(
+    tx_peer: SendMsg,
+    url_stream: impl futures::Stream<Item = Url> + Send + 'static,
+    max_parallel_requests: BoundedU16<1, { u16::MAX }>,
+    request_timeout_duration: Duration,
+) {
+    use futures::StreamExt;
+
+    // Note that `tokio` - the de facto standard async runtime - is not supported on WASM. We need
+    // to spawn tasks for HTTP requests, and for WASM we rely on the `wasm_bindgen_futures` crate.
+    #[cfg(not(target_arch = "wasm32"))]
+    let spawn_fn = tokio::spawn;
+
+    #[cfg(target_arch = "wasm32")]
+    let spawn_fn = wasm_bindgen_futures::spawn_local;
+
+    let mapped_stream = url_stream
+        .map(move |mut url| {
+            let mut tx_peer = tx_peer.clone();
+            async move {
+                // `tokio::spawn` returns a `JoinHandle` which we make sure to drop. If we don't drop
+                // and instead await on it, performance suffers greatly (~ 5x slower). In WASM case
+                // we don't need to worry because `wasm_bindgen_futures::spawn_local` returns ().
+                let _handle = spawn_fn(async move {
+                    // Query node at url.
+                    #[allow(clippy::unwrap_used)]
+                    url.set_port(Some(9053)).unwrap();
+                    #[allow(clippy::unwrap_used)]
+                    let node_conf = NodeConf {
+                        addr: PeerAddr(url.socket_addrs(|| Some(9053)).unwrap()[0]),
+                        api_key: None,
+                        timeout: Some(request_timeout_duration),
+                    };
+
+                    // If active, look up its peers.
+                    if get_info(node_conf).await.is_ok() {
+                        match get_peers_all(node_conf).await {
+                            Ok(peers) => {
+                                // It's important to send this message before the `AddActiveNode`
+                                // message below, to ensure an accurate `count` variable in task 1;
+                                // see (*) above in `peer_discovery_inner`.
+                                tx_peer.infallible_send(Msg::CheckPeers(peers)).await;
+                                tx_peer
+                                    .infallible_send(Msg::AddActiveNode(url.clone()))
+                                    .await;
+                            }
+                            Err(_) => {
+                                #[allow(clippy::unwrap_used)]
+                                tx_peer.infallible_send(Msg::AddInactiveNode(url)).await;
+                            }
+                        }
+                    } else {
+                        #[allow(clippy::unwrap_used)]
+                        tx_peer.infallible_send(Msg::AddInactiveNode(url)).await;
+                    }
+                });
+            }
+        })
+        .buffer_unordered(max_parallel_requests.get() as usize); // Allow for parallel requests
+
+    // Note: We need to define another binding to the spawn function to get around the Rust type
+    // checker.
+    #[cfg(not(target_arch = "wasm32"))]
+    let spawn_fn_new = tokio::spawn;
+
+    #[cfg(target_arch = "wasm32")]
+    let spawn_fn_new = wasm_bindgen_futures::spawn_local;
+
+    // (*) Run stream to completion.
+    spawn_fn_new(mapped_stream.for_each(|_| async move {}));
+}
+
+/// GET on /peers/all endpoint
+async fn get_peers_all(node: NodeConf) -> Result<Vec<PeerInfo>, NodeError> {
+    #[allow(clippy::unwrap_used)]
+    let url = node.addr.as_http_url().join("peers/all").unwrap();
+    let client = build_client(&node)?;
+    let rb = client.get(url);
+    let response = set_req_headers(rb, node).send().await?;
+    Ok(response.json::<Vec<PeerInfo>>().await?)
+}
+
+/// Used in the implementation of `peer_discovery`
+#[derive(Debug)]
+pub(crate) enum Msg {
+    /// Indicates that the ergo node at the given URL is active. This means that a GET request
+    /// to the node's /info endpoint responds with code 200 OK.
+    AddActiveNode(Url),
+    /// Indicates that the ergo node at the given URL is inactive. This means that a GET request
+    /// to the node's /info endpoint does not respond with code 200 OK.
+    AddInactiveNode(Url),
+    /// A list of peers of an active ergo node, returned from a GET on the /peers/all endpoint.
+    CheckPeers(Vec<PeerInfo>),
+}
+
+/// This trait abstracts over the `send` method of channel senders, assuming no failure.
+#[async_trait]
+trait ChannelInfallibleSender<T> {
+    /// A send that cannot fail.
+    async fn infallible_send(&mut self, value: T);
+}
+
+#[cfg(not(target_arch = "wasm32"))]
+#[async_trait]
+impl<T: Debug + Send> ChannelInfallibleSender<T> for tokio::sync::mpsc::Sender<T> {
+    async fn infallible_send(&mut self, value: T) {
+        // If error results, just discard it.
+        let _ = self.send(value).await;
+    }
+}
+
+#[cfg(target_arch = "wasm32")]
+#[async_trait]
+impl<T: Debug + Send> ChannelInfallibleSender<T> for futures::channel::mpsc::Sender<T> {
+    async fn infallible_send(&mut self, value: T) {
+        use futures::sink::SinkExt;
+        // If error results, just discard it.
+        let _ = self.send(value).await;
+    }
+}
+
+/// This trait abstracts over the `try_send` method of channel senders
+trait ChannelTrySender<T> {
+    fn try_send(&mut self, value: T) -> Result<(), TrySendError>;
+}
+
+/// Errors that can return from `try_send(..)` calls are converted into the following enum.
+enum TrySendError {
+    /// Receiver's buffer is full
+    Full,
+    /// Receiver is no longer active. Either it was specifically closed or dropped.
+    Closed,
+}
+
+#[cfg(not(target_arch = "wasm32"))]
+impl<T> ChannelTrySender<T> for tokio::sync::mpsc::Sender<T> {
+    fn try_send(&mut self, value: T) -> Result<(), TrySendError> {
+        use tokio::sync::mpsc::error::TrySendError as TokioTrySendError;
+        match tokio::sync::mpsc::Sender::try_send(self, value) {
+            Ok(()) => Ok(()),
+            Err(TokioTrySendError::Full(_)) => Err(TrySendError::Full),
+            Err(TokioTrySendError::Closed(_)) => Err(TrySendError::Closed),
+        }
+    }
+}
+
+#[cfg(target_arch = "wasm32")]
+impl<T> ChannelTrySender<T> for futures::channel::mpsc::Sender<T> {
+    fn try_send(&mut self, value: T) -> Result<(), TrySendError> {
+        match futures::channel::mpsc::Sender::try_send(self, value) {
+            Ok(_) => Ok(()),
+            Err(e) => {
+                if e.is_full() {
+                    Err(TrySendError::Full)
+                } else {
+                    Err(TrySendError::Closed)
+                }
+            }
+        }
+    }
+}
+
+#[cfg(test)]
+#[allow(clippy::unwrap_used)]
+mod tests {
+    use super::*;
+    use std::str::FromStr;
+
+    #[test]
+    fn test_get_peers_all() {
+        let runtime_inner = tokio::runtime::Builder::new_multi_thread()
+            .enable_all()
+            .build()
+            .unwrap();
+        let node_conf = NodeConf {
+            addr: PeerAddr::from_str("213.239.193.208:9053").unwrap(),
+            api_key: None,
+            timeout: Some(Duration::from_secs(5)),
+        };
+        let res = runtime_inner.block_on(async { get_peers_all(node_conf).await.unwrap() });
+        assert!(!res.is_empty())
+    }
+}
diff --git a/ergo-rest/src/error.rs b/ergo-rest/src/error.rs
index f8c0bb3dc..54e44e911 100644
--- a/ergo-rest/src/error.rs
+++ b/ergo-rest/src/error.rs
@@ -6,4 +6,24 @@ pub enum NodeError {
     /// reqwest error
     #[error("reqwest error: {0}")]
     ReqwestError(#[from] reqwest::Error),
+    /// Invalid numerical URL segment
+    #[error("Invalid numerical URL segment")]
+    InvalidNumericalUrlSegment,
+}
+
+#[derive(Debug, Error)]
+/// Peer discovery error
+pub enum PeerDiscoveryError {
+    /// `Url` error
+    #[error("URL error")]
+    UrlError,
+    /// mpsc sender error
+    #[error("MPSC sender error")]
+    MpscSender,
+    /// tokio::spawn `JoinError`
+    #[error("Join error")]
+    JoinError,
+    /// task spawn error
+    #[error("Task spawn error")]
+    TaskSpawn,
 }
diff --git a/ergo-rest/src/lib.rs b/ergo-rest/src/lib.rs
index 558079bd1..abbd5d066 100644
--- a/ergo-rest/src/lib.rs
+++ b/ergo-rest/src/lib.rs
@@ -28,7 +28,7 @@ mod peer_info;
 
 pub mod api;
 
-pub use error::NodeError;
+pub use error::*;
 pub use known_nodes::KnownNodes;
 pub use node_conf::NodeConf;
 pub use node_info::NodeInfo;
diff --git a/ergo-rest/src/peer_info.rs b/ergo-rest/src/peer_info.rs
index e69de4863..a5358176e 100644
--- a/ergo-rest/src/peer_info.rs
+++ b/ergo-rest/src/peer_info.rs
@@ -1,14 +1,57 @@
 use ergo_chain_types::ConnectionDirection;
 use ergo_chain_types::PeerAddr;
+use serde::{de::Error, Deserialize, Deserializer, Serialize};
 
+#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
+#[repr(C)]
 /// Peer info returned by node REST API
 pub struct PeerInfo {
     /// Peer address
+    #[serde(
+        rename = "address",
+        deserialize_with = "parse_peer_addr_with_leading_slash"
+    )]
     pub addr: PeerAddr,
+    /// Last message
+    #[serde(rename = "lastMessage")]
+    pub last_message: u64,
     /// Timestamp of the last handshake
+    #[serde(rename = "lastHandshake")]
     pub last_handshake: u64, // TODO: any more precise type?
     /// Node name
     pub name: String,
     /// Type of connection (Incoming/Outgoing) established to this peer if any
+    #[serde(rename = "connectionType")]
     pub conn_type: Option<ConnectionDirection>,
 }
+
+/// The `PeerAddr` reported by ergo nodes at the `peers/all` endpoint begin with a leading '/'.
+/// This custom deserialization function simply removes the leading character then uses the
+/// serde-generated function to parse.
+fn parse_peer_addr_with_leading_slash<'de, D>(deserializer: D) -> Result<PeerAddr, D::Error>
+where
+    D: Deserializer<'de>,
+{
+    let s: &str = Deserialize::deserialize(deserializer)?;
+
+    let s = if s.starts_with('/') {
+        let mut chars = s.chars();
+        chars.next();
+        chars.as_str()
+    } else {
+        s
+    };
+    s.parse().map_err(D::Error::custom)
+}
+
+#[cfg(test)]
+#[cfg(feature = "json")]
+#[allow(clippy::unwrap_used)]
+mod tests {
+    use super::*;
+    #[test]
+    fn test_parse_peer_info() {
+        let json = "{\n    \"address\" : \"/62.106.112.158:9030\",\n    \"lastMessage\" : 0,\n    \"lastHandshake\" : 0,\n    \"name\" : \"ergo-mainnet-4.0.12\",\n    \"connectionType\" : null\n  }";
+        let _: PeerInfo = serde_json::from_str(json).unwrap();
+    }
+}
diff --git a/ergotree-interpreter/Cargo.toml b/ergotree-interpreter/Cargo.toml
index 205ad6d89..ea62a3833 100644
--- a/ergotree-interpreter/Cargo.toml
+++ b/ergotree-interpreter/Cargo.toml
@@ -33,8 +33,7 @@ base16 = "0.2.1"
 bytes = "1.1"
 num-bigint = "0.4.0"
 scorex_crypto_avltree = "0.1.0"
-#bounded-vec = { git = "https://github.com/ergoplatform/bounded-vec", rev="f684d78" }
-bounded-vec = { version = "^0.5.0", features=["serde"] }
+bounded-vec = { version = "^0.6.0", features=["serde"] }
 
 [dependencies.proptest]
 # wasm support, via https://altsysrq.github.io/proptest-book/proptest/wasm.html
diff --git a/ergotree-ir/Cargo.toml b/ergotree-ir/Cargo.toml
index 43e1238c8..da751a9e0 100644
--- a/ergotree-ir/Cargo.toml
+++ b/ergotree-ir/Cargo.toml
@@ -30,8 +30,7 @@ bs58 = "0.4.0"
 base16 = "0.2.1"
 byteorder = "1"
 num-bigint = "0.4.0"
-#bounded-vec = { git = "https://github.com/ergoplatform/bounded-vec", rev="f684d78" }
-bounded-vec = { version = "^0.5.0", features=["serde"] }
+bounded-vec = { version = "^0.6.0", features=["serde"] }
 num256 = "0.3.1"
 num-traits = "0.2.14"
 num-derive = "0.3.3"
diff --git a/sigma-ser/Cargo.toml b/sigma-ser/Cargo.toml
index ce49ee759..6325ff28a 100644
--- a/sigma-ser/Cargo.toml
+++ b/sigma-ser/Cargo.toml
@@ -17,7 +17,7 @@ crate-type = ["cdylib", "rlib"]
 # wasm-bindgen = "0.2"
 thiserror = "1"
 bitvec = "0.22.3"
-bounded-vec = { version = "^0.5.0" }
+bounded-vec = { version = "^0.6.0" }
 # The `console_error_panic_hook` crate provides better debugging of panics by
 # logging them with `console.error`. This is great for development, but requires
 # all the `std::fmt` and `std::panicking` infrastructure, so isn't great for
diff --git a/vendored-deps/reqwest-wrap/Cargo.toml b/vendored-deps/reqwest-wrap/Cargo.toml
new file mode 100644
index 000000000..497b659ce
--- /dev/null
+++ b/vendored-deps/reqwest-wrap/Cargo.toml
@@ -0,0 +1,50 @@
+[package]
+name = "reqwest-wrap"
+version = "0.11.10"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[features]
+json = ["serde_json"]
+
+[dependencies]
+http = "0.2"
+url = "2.2"
+bytes = "1.0"
+serde = "1.0"
+serde_urlencoded = "0.7.1"
+serde_json = { version = "1.0", optional = true }
+
+[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
+reqwest = { version = "0.11.10", default-features = false, features = ["json"] }
+
+[target.'cfg(target_arch = "wasm32")'.dependencies]
+js-sys = "0.3.45"
+serde_json = "1.0"
+wasm-bindgen = "0.2.68"
+wasm-bindgen-futures = "0.4.18"
+
+
+[target.'cfg(target_arch = "wasm32")'.dependencies.web-sys]
+version = "0.3.25"
+features = [
+    "Headers",
+    "Request",
+    "RequestInit",
+    "RequestMode",
+    "AbortController",
+    "AbortSignal",
+    "Response",
+    "Window",
+    "FormData",
+    "Blob",
+    "BlobPropertyBag",
+    "ServiceWorkerGlobalScope",
+    "RequestCredentials",
+    "File"
+]
+
+[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
+wasm-bindgen = { version = "0.2.68", features = ["serde-serialize"] }
+wasm-bindgen-test = "0.3"
\ No newline at end of file
diff --git a/vendored-deps/reqwest-wrap/LICENSE-APACHE b/vendored-deps/reqwest-wrap/LICENSE-APACHE
new file mode 100644
index 000000000..16fe87b06
--- /dev/null
+++ b/vendored-deps/reqwest-wrap/LICENSE-APACHE
@@ -0,0 +1,201 @@
+                              Apache License
+                        Version 2.0, January 2004
+                     http://www.apache.org/licenses/
+
+TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+1. Definitions.
+
+   "License" shall mean the terms and conditions for use, reproduction,
+   and distribution as defined by Sections 1 through 9 of this document.
+
+   "Licensor" shall mean the copyright owner or entity authorized by
+   the copyright owner that is granting the License.
+
+   "Legal Entity" shall mean the union of the acting entity and all
+   other entities that control, are controlled by, or are under common
+   control with that entity. For the purposes of this definition,
+   "control" means (i) the power, direct or indirect, to cause the
+   direction or management of such entity, whether by contract or
+   otherwise, or (ii) ownership of fifty percent (50%) or more of the
+   outstanding shares, or (iii) beneficial ownership of such entity.
+
+   "You" (or "Your") shall mean an individual or Legal Entity
+   exercising permissions granted by this License.
+
+   "Source" form shall mean the preferred form for making modifications,
+   including but not limited to software source code, documentation
+   source, and configuration files.
+
+   "Object" form shall mean any form resulting from mechanical
+   transformation or translation of a Source form, including but
+   not limited to compiled object code, generated documentation,
+   and conversions to other media types.
+
+   "Work" shall mean the work of authorship, whether in Source or
+   Object form, made available under the License, as indicated by a
+   copyright notice that is included in or attached to the work
+   (an example is provided in the Appendix below).
+
+   "Derivative Works" shall mean any work, whether in Source or Object
+   form, that is based on (or derived from) the Work and for which the
+   editorial revisions, annotations, elaborations, or other modifications
+   represent, as a whole, an original work of authorship. For the purposes
+   of this License, Derivative Works shall not include works that remain
+   separable from, or merely link (or bind by name) to the interfaces of,
+   the Work and Derivative Works thereof.
+
+   "Contribution" shall mean any work of authorship, including
+   the original version of the Work and any modifications or additions
+   to that Work or Derivative Works thereof, that is intentionally
+   submitted to Licensor for inclusion in the Work by the copyright owner
+   or by an individual or Legal Entity authorized to submit on behalf of
+   the copyright owner. For the purposes of this definition, "submitted"
+   means any form of electronic, verbal, or written communication sent
+   to the Licensor or its representatives, including but not limited to
+   communication on electronic mailing lists, source code control systems,
+   and issue tracking systems that are managed by, or on behalf of, the
+   Licensor for the purpose of discussing and improving the Work, but
+   excluding communication that is conspicuously marked or otherwise
+   designated in writing by the copyright owner as "Not a Contribution."
+
+   "Contributor" shall mean Licensor and any individual or Legal Entity
+   on behalf of whom a Contribution has been received by Licensor and
+   subsequently incorporated within the Work.
+
+2. Grant of Copyright License. Subject to the terms and conditions of
+   this License, each Contributor hereby grants to You a perpetual,
+   worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+   copyright license to reproduce, prepare Derivative Works of,
+   publicly display, publicly perform, sublicense, and distribute the
+   Work and such Derivative Works in Source or Object form.
+
+3. Grant of Patent License. Subject to the terms and conditions of
+   this License, each Contributor hereby grants to You a perpetual,
+   worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+   (except as stated in this section) patent license to make, have made,
+   use, offer to sell, sell, import, and otherwise transfer the Work,
+   where such license applies only to those patent claims licensable
+   by such Contributor that are necessarily infringed by their
+   Contribution(s) alone or by combination of their Contribution(s)
+   with the Work to which such Contribution(s) was submitted. If You
+   institute patent litigation against any entity (including a
+   cross-claim or counterclaim in a lawsuit) alleging that the Work
+   or a Contribution incorporated within the Work constitutes direct
+   or contributory patent infringement, then any patent licenses
+   granted to You under this License for that Work shall terminate
+   as of the date such litigation is filed.
+
+4. Redistribution. You may reproduce and distribute copies of the
+   Work or Derivative Works thereof in any medium, with or without
+   modifications, and in Source or Object form, provided that You
+   meet the following conditions:
+
+   (a) You must give any other recipients of the Work or
+       Derivative Works a copy of this License; and
+
+   (b) You must cause any modified files to carry prominent notices
+       stating that You changed the files; and
+
+   (c) You must retain, in the Source form of any Derivative Works
+       that You distribute, all copyright, patent, trademark, and
+       attribution notices from the Source form of the Work,
+       excluding those notices that do not pertain to any part of
+       the Derivative Works; and
+
+   (d) If the Work includes a "NOTICE" text file as part of its
+       distribution, then any Derivative Works that You distribute must
+       include a readable copy of the attribution notices contained
+       within such NOTICE file, excluding those notices that do not
+       pertain to any part of the Derivative Works, in at least one
+       of the following places: within a NOTICE text file distributed
+       as part of the Derivative Works; within the Source form or
+       documentation, if provided along with the Derivative Works; or,
+       within a display generated by the Derivative Works, if and
+       wherever such third-party notices normally appear. The contents
+       of the NOTICE file are for informational purposes only and
+       do not modify the License. You may add Your own attribution
+       notices within Derivative Works that You distribute, alongside
+       or as an addendum to the NOTICE text from the Work, provided
+       that such additional attribution notices cannot be construed
+       as modifying the License.
+
+   You may add Your own copyright statement to Your modifications and
+   may provide additional or different license terms and conditions
+   for use, reproduction, or distribution of Your modifications, or
+   for any such Derivative Works as a whole, provided Your use,
+   reproduction, and distribution of the Work otherwise complies with
+   the conditions stated in this License.
+
+5. Submission of Contributions. Unless You explicitly state otherwise,
+   any Contribution intentionally submitted for inclusion in the Work
+   by You to the Licensor shall be under the terms and conditions of
+   this License, without any additional terms or conditions.
+   Notwithstanding the above, nothing herein shall supersede or modify
+   the terms of any separate license agreement you may have executed
+   with Licensor regarding such Contributions.
+
+6. Trademarks. This License does not grant permission to use the trade
+   names, trademarks, service marks, or product names of the Licensor,
+   except as required for reasonable and customary use in describing the
+   origin of the Work and reproducing the content of the NOTICE file.
+
+7. Disclaimer of Warranty. Unless required by applicable law or
+   agreed to in writing, Licensor provides the Work (and each
+   Contributor provides its Contributions) on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+   implied, including, without limitation, any warranties or conditions
+   of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+   PARTICULAR PURPOSE. You are solely responsible for determining the
+   appropriateness of using or redistributing the Work and assume any
+   risks associated with Your exercise of permissions under this License.
+
+8. Limitation of Liability. In no event and under no legal theory,
+   whether in tort (including negligence), contract, or otherwise,
+   unless required by applicable law (such as deliberate and grossly
+   negligent acts) or agreed to in writing, shall any Contributor be
+   liable to You for damages, including any direct, indirect, special,
+   incidental, or consequential damages of any character arising as a
+   result of this License or out of the use or inability to use the
+   Work (including but not limited to damages for loss of goodwill,
+   work stoppage, computer failure or malfunction, or any and all
+   other commercial damages or losses), even if such Contributor
+   has been advised of the possibility of such damages.
+
+9. Accepting Warranty or Additional Liability. While redistributing
+   the Work or Derivative Works thereof, You may choose to offer,
+   and charge a fee for, acceptance of support, warranty, indemnity,
+   or other liability obligations and/or rights consistent with this
+   License. However, in accepting such obligations, You may act only
+   on Your own behalf and on Your sole responsibility, not on behalf
+   of any other Contributor, and only if You agree to indemnify,
+   defend, and hold each Contributor harmless for any liability
+   incurred by, or claims asserted against, such Contributor by reason
+   of your accepting any such warranty or additional liability.
+
+END OF TERMS AND CONDITIONS
+
+APPENDIX: How to apply the Apache License to your work.
+
+   To apply the Apache License to your work, attach the following
+   boilerplate notice, with the fields enclosed by brackets "[]"
+   replaced with your own identifying information. (Don't include
+   the brackets!)  The text should be enclosed in the appropriate
+   comment syntax for the file format. We also recommend that a
+   file or class name and description of purpose be included on the
+   same "printed page" as the copyright notice for easier
+   identification within third-party archives.
+
+Copyright [yyyy] [name of copyright owner]
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+	http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
diff --git a/vendored-deps/reqwest-wrap/LICENSE-MIT b/vendored-deps/reqwest-wrap/LICENSE-MIT
new file mode 100644
index 000000000..4e45f06d6
--- /dev/null
+++ b/vendored-deps/reqwest-wrap/LICENSE-MIT
@@ -0,0 +1,20 @@
+Copyright (c) 2016 Sean McArthur
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
diff --git a/vendored-deps/reqwest-wrap/README.md b/vendored-deps/reqwest-wrap/README.md
new file mode 100644
index 000000000..ebeb162ab
--- /dev/null
+++ b/vendored-deps/reqwest-wrap/README.md
@@ -0,0 +1,8 @@
+This crate exists as a workaround that gives `reqwest` the ability to have request timeouts for the
+WASM platform (see https://github.com/seanmonstar/reqwest/issues/1135). Currently timeouts are
+only implemented in `reqwest` for non-WASM platforms. However there exists a yet-to-be-merged pull
+request (https://github.com/seanmonstar/reqwest/pull/1274) that implements timeouts under WASM.
+
+This crate vendors the WASM related code of `reqwest` and applies the above mentioned PR on top.
+This allows WASM-based dependendents of this crate to have timeouts for requests. Non-WASM users
+have access to the underlying `reqwest` crate without any modifications.
\ No newline at end of file
diff --git a/vendored-deps/reqwest-wrap/src/error.rs b/vendored-deps/reqwest-wrap/src/error.rs
new file mode 100644
index 000000000..45a5c063c
--- /dev/null
+++ b/vendored-deps/reqwest-wrap/src/error.rs
@@ -0,0 +1,366 @@
+#![cfg_attr(target_arch = "wasm32", allow(unused))]
+use std::error::Error as StdError;
+use std::fmt;
+use std::io;
+
+use crate::{StatusCode, Url};
+
+/// A `Result` alias where the `Err` case is `reqwest::Error`.
+pub type Result<T> = std::result::Result<T, Error>;
+
+/// The Errors that may occur when processing a `Request`.
+///
+/// Note: Errors may include the full URL used to make the `Request`. If the URL
+/// contains sensitive information (e.g. an API key as a query parameter), be
+/// sure to remove it ([`without_url`](Error::without_url))
+pub struct Error {
+    inner: Box<Inner>,
+}
+
+pub(crate) type BoxError = Box<dyn StdError + Send + Sync>;
+
+struct Inner {
+    kind: Kind,
+    source: Option<BoxError>,
+    url: Option<Url>,
+}
+
+impl Error {
+    pub(crate) fn new<E>(kind: Kind, source: Option<E>) -> Error
+    where
+        E: Into<BoxError>,
+    {
+        Error {
+            inner: Box::new(Inner {
+                kind,
+                source: source.map(Into::into),
+                url: None,
+            }),
+        }
+    }
+
+    /// Returns a possible URL related to this error.
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// # async fn run() {
+    /// // displays last stop of a redirect loop
+    /// let response = reqwest::get("http://site.with.redirect.loop").await;
+    /// if let Err(e) = response {
+    ///     if e.is_redirect() {
+    ///         if let Some(final_stop) = e.url() {
+    ///             println!("redirect loop at {}", final_stop);
+    ///         }
+    ///     }
+    /// }
+    /// # }
+    /// ```
+    pub fn url(&self) -> Option<&Url> {
+        self.inner.url.as_ref()
+    }
+
+    /// Returns a mutable referene to the URL related to this error
+    ///
+    /// This is useful if you need to remove sensitive information from the URL
+    /// (e.g. an API key in the query), but do not want to remove the URL
+    /// entirely.
+    pub fn url_mut(&mut self) -> Option<&mut Url> {
+        self.inner.url.as_mut()
+    }
+
+    /// Add a url related to this error (overwriting any existing)
+    pub fn with_url(mut self, url: Url) -> Self {
+        self.inner.url = Some(url);
+        self
+    }
+
+    /// Strip the related url from this error (if, for example, it contains
+    /// sensitive information)
+    pub fn without_url(mut self) -> Self {
+        self.inner.url = None;
+        self
+    }
+
+    /// Returns true if the error is from a type Builder.
+    pub fn is_builder(&self) -> bool {
+        matches!(self.inner.kind, Kind::Builder)
+    }
+
+    /// Returns true if the error is from a `RedirectPolicy`.
+    pub fn is_redirect(&self) -> bool {
+        matches!(self.inner.kind, Kind::Redirect)
+    }
+
+    /// Returns true if the error is from `Response::error_for_status`.
+    pub fn is_status(&self) -> bool {
+        matches!(self.inner.kind, Kind::Status(_))
+    }
+
+    /// Returns true if the error is related to a timeout.
+    pub fn is_timeout(&self) -> bool {
+        let mut source = self.source();
+
+        while let Some(err) = source {
+            if err.is::<TimedOut>() {
+                return true;
+            }
+            source = err.source();
+        }
+
+        false
+    }
+
+    /// Returns true if the error is related to the request
+    pub fn is_request(&self) -> bool {
+        matches!(self.inner.kind, Kind::Request)
+    }
+
+    #[cfg(not(target_arch = "wasm32"))]
+    /// Returns true if the error is related to connect
+    pub fn is_connect(&self) -> bool {
+        let mut source = self.source();
+
+        while let Some(err) = source {
+            if let Some(hyper_err) = err.downcast_ref::<hyper::Error>() {
+                if hyper_err.is_connect() {
+                    return true;
+                }
+            }
+
+            source = err.source();
+        }
+
+        false
+    }
+
+    /// Returns true if the error is related to the request or response body
+    pub fn is_body(&self) -> bool {
+        matches!(self.inner.kind, Kind::Body)
+    }
+
+    /// Returns true if the error is related to decoding the response's body
+    pub fn is_decode(&self) -> bool {
+        matches!(self.inner.kind, Kind::Decode)
+    }
+
+    /// Returns the status code, if the error was generated from a response.
+    pub fn status(&self) -> Option<StatusCode> {
+        match self.inner.kind {
+            Kind::Status(code) => Some(code),
+            _ => None,
+        }
+    }
+
+    // private
+
+    #[allow(unused)]
+    pub(crate) fn into_io(self) -> io::Error {
+        io::Error::new(io::ErrorKind::Other, self)
+    }
+}
+
+impl fmt::Debug for Error {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        let mut builder = f.debug_struct("reqwest::Error");
+
+        builder.field("kind", &self.inner.kind);
+
+        if let Some(ref url) = self.inner.url {
+            builder.field("url", url);
+        }
+        if let Some(ref source) = self.inner.source {
+            builder.field("source", source);
+        }
+
+        builder.finish()
+    }
+}
+
+impl fmt::Display for Error {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self.inner.kind {
+            Kind::Builder => f.write_str("builder error")?,
+            Kind::Request => f.write_str("error sending request")?,
+            Kind::Body => f.write_str("request or response body error")?,
+            Kind::Decode => f.write_str("error decoding response body")?,
+            Kind::Redirect => f.write_str("error following redirect")?,
+            Kind::Status(ref code) => {
+                let prefix = if code.is_client_error() {
+                    "HTTP status client error"
+                } else {
+                    debug_assert!(code.is_server_error());
+                    "HTTP status server error"
+                };
+                write!(f, "{} ({})", prefix, code)?;
+            }
+        };
+
+        if let Some(url) = &self.inner.url {
+            write!(f, " for url ({})", url.as_str())?;
+        }
+
+        if let Some(e) = &self.inner.source {
+            write!(f, ": {}", e)?;
+        }
+
+        Ok(())
+    }
+}
+
+impl StdError for Error {
+    fn source(&self) -> Option<&(dyn StdError + 'static)> {
+        self.inner.source.as_ref().map(|e| &**e as _)
+    }
+}
+
+#[cfg(target_arch = "wasm32")]
+impl From<crate::error::Error> for wasm_bindgen::JsValue {
+    fn from(err: Error) -> wasm_bindgen::JsValue {
+        js_sys::Error::from(err).into()
+    }
+}
+
+#[cfg(target_arch = "wasm32")]
+impl From<crate::error::Error> for js_sys::Error {
+    fn from(err: Error) -> js_sys::Error {
+        js_sys::Error::new(&format!("{}", err))
+    }
+}
+
+#[derive(Debug)]
+pub(crate) enum Kind {
+    Builder,
+    Request,
+    Redirect,
+    Status(StatusCode),
+    Body,
+    Decode,
+}
+
+// constructors
+
+pub(crate) fn builder<E: Into<BoxError>>(e: E) -> Error {
+    Error::new(Kind::Builder, Some(e))
+}
+
+pub(crate) fn body<E: Into<BoxError>>(e: E) -> Error {
+    Error::new(Kind::Body, Some(e))
+}
+
+pub(crate) fn decode<E: Into<BoxError>>(e: E) -> Error {
+    Error::new(Kind::Decode, Some(e))
+}
+
+pub(crate) fn request<E: Into<BoxError>>(e: E) -> Error {
+    Error::new(Kind::Request, Some(e))
+}
+
+pub(crate) fn redirect<E: Into<BoxError>>(e: E, url: Url) -> Error {
+    Error::new(Kind::Redirect, Some(e)).with_url(url)
+}
+
+pub(crate) fn status_code(url: Url, status: StatusCode) -> Error {
+    Error::new(Kind::Status(status), None::<Error>).with_url(url)
+}
+
+pub(crate) fn url_bad_scheme(url: Url) -> Error {
+    Error::new(Kind::Builder, Some("URL scheme is not allowed")).with_url(url)
+}
+
+if_wasm! {
+    pub(crate) fn wasm(js_val: wasm_bindgen::JsValue) -> BoxError {
+        format!("{:?}", js_val).into()
+    }
+}
+
+// io::Error helpers
+
+#[allow(unused)]
+pub(crate) fn into_io(e: Error) -> io::Error {
+    e.into_io()
+}
+
+#[allow(unused)]
+pub(crate) fn decode_io(e: io::Error) -> Error {
+    if e.get_ref().map(|r| r.is::<Error>()).unwrap_or(false) {
+        *e.into_inner()
+            .expect("io::Error::get_ref was Some(_)")
+            .downcast::<Error>()
+            .expect("StdError::is() was true")
+    } else {
+        decode(e)
+    }
+}
+
+// internal Error "sources"
+
+#[derive(Debug)]
+pub(crate) struct TimedOut;
+
+impl fmt::Display for TimedOut {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        f.write_str("operation timed out")
+    }
+}
+
+impl StdError for TimedOut {}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    fn assert_send<T: Send>() {}
+    fn assert_sync<T: Sync>() {}
+
+    #[test]
+    fn test_source_chain() {
+        let root = Error::new(Kind::Request, None::<Error>);
+        assert!(root.source().is_none());
+
+        let link = super::body(root);
+        assert!(link.source().is_some());
+        assert_send::<Error>();
+        assert_sync::<Error>();
+    }
+
+    #[test]
+    fn mem_size_of() {
+        use std::mem::size_of;
+        assert_eq!(size_of::<Error>(), size_of::<usize>());
+    }
+
+    #[test]
+    fn roundtrip_io_error() {
+        let orig = super::request("orig");
+        // Convert reqwest::Error into an io::Error...
+        let io = orig.into_io();
+        // Convert that io::Error back into a reqwest::Error...
+        let err = super::decode_io(io);
+        // It should have pulled out the original, not nested it...
+        match err.inner.kind {
+            Kind::Request => (),
+            _ => panic!("{:?}", err),
+        }
+    }
+
+    #[test]
+    fn from_unknown_io_error() {
+        let orig = io::Error::new(io::ErrorKind::Other, "orly");
+        let err = super::decode_io(orig);
+        match err.inner.kind {
+            Kind::Decode => (),
+            _ => panic!("{:?}", err),
+        }
+    }
+
+    #[test]
+    fn is_timeout() {
+        let err = super::request(super::TimedOut);
+        assert!(err.is_timeout());
+
+        let io = io::Error::new(io::ErrorKind::Other, err);
+        let nested = super::request(io);
+        assert!(nested.is_timeout());
+    }
+}
diff --git a/vendored-deps/reqwest-wrap/src/into_url.rs b/vendored-deps/reqwest-wrap/src/into_url.rs
new file mode 100644
index 000000000..ef0db9495
--- /dev/null
+++ b/vendored-deps/reqwest-wrap/src/into_url.rs
@@ -0,0 +1,90 @@
+use url::Url;
+
+/// A trait to try to convert some type into a `Url`.
+///
+/// This trait is "sealed", such that only types within reqwest can
+/// implement it.
+pub trait IntoUrl: IntoUrlSealed {}
+
+impl IntoUrl for Url {}
+impl IntoUrl for String {}
+impl<'a> IntoUrl for &'a str {}
+impl<'a> IntoUrl for &'a String {}
+
+pub trait IntoUrlSealed {
+    // Besides parsing as a valid `Url`, the `Url` must be a valid
+    // `http::Uri`, in that it makes sense to use in a network request.
+    fn into_url(self) -> crate::Result<Url>;
+
+    fn as_str(&self) -> &str;
+}
+
+impl IntoUrlSealed for Url {
+    fn into_url(self) -> crate::Result<Url> {
+        if self.has_host() {
+            Ok(self)
+        } else {
+            Err(crate::error::url_bad_scheme(self))
+        }
+    }
+
+    fn as_str(&self) -> &str {
+        self.as_ref()
+    }
+}
+
+impl<'a> IntoUrlSealed for &'a str {
+    fn into_url(self) -> crate::Result<Url> {
+        Url::parse(self).map_err(crate::error::builder)?.into_url()
+    }
+
+    fn as_str(&self) -> &str {
+        self
+    }
+}
+
+impl<'a> IntoUrlSealed for &'a String {
+    fn into_url(self) -> crate::Result<Url> {
+        (&**self).into_url()
+    }
+
+    fn as_str(&self) -> &str {
+        self.as_ref()
+    }
+}
+
+impl<'a> IntoUrlSealed for String {
+    fn into_url(self) -> crate::Result<Url> {
+        (&*self).into_url()
+    }
+
+    fn as_str(&self) -> &str {
+        self.as_ref()
+    }
+}
+
+if_hyper! {
+    pub(crate) fn expect_uri(url: &Url) -> http::Uri {
+        url.as_str()
+            .parse()
+            .expect("a parsed Url should always be a valid Uri")
+    }
+
+    pub(crate) fn try_uri(url: &Url) -> Option<http::Uri> {
+        url.as_str().parse().ok()
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn into_url_file_scheme() {
+        let err = "file:///etc/hosts".into_url().unwrap_err();
+        assert_eq!(
+            err.to_string(),
+            "builder error for url (file:///etc/hosts): URL scheme is not allowed"
+        );
+    }
+}
diff --git a/vendored-deps/reqwest-wrap/src/lib.rs b/vendored-deps/reqwest-wrap/src/lib.rs
new file mode 100644
index 000000000..edeb7a341
--- /dev/null
+++ b/vendored-deps/reqwest-wrap/src/lib.rs
@@ -0,0 +1,68 @@
+macro_rules! if_wasm {
+    ($($item:item)*) => {$(
+        #[cfg(target_arch = "wasm32")]
+        $item
+    )*}
+}
+
+macro_rules! if_hyper {
+    ($($item:item)*) => {$(
+        #[cfg(not(target_arch = "wasm32"))]
+        $item
+    )*}
+}
+
+pub use http::header;
+
+if_hyper! {
+    pub use reqwest::{
+        Body, Client, ClientBuilder, Request, RequestBuilder, Response, Error, Result
+    };
+}
+
+if_wasm! {
+    mod wasm;
+    mod util;
+    #[macro_use]
+    mod error;
+    mod into_url;
+    mod response;
+    pub use self::error::{Error, Result};
+    pub use self::into_url::IntoUrl;
+    pub use self::response::ResponseBuilderExt;
+    pub use http::Method;
+    pub use http::{StatusCode, Version};
+    pub use url::Url;
+    pub use self::wasm::{Body, Client, ClientBuilder, Request, RequestBuilder, Response};
+
+    /// Shortcut method to quickly make a `GET` request.
+    ///
+    /// See also the methods on the [`reqwest::Response`](./struct.Response.html)
+    /// type.
+    ///
+    /// **NOTE**: This function creates a new internal `Client` on each call,
+    /// and so should not be used if making many requests. Create a
+    /// [`Client`](./struct.Client.html) instead.
+    ///
+    /// # Examples
+    ///
+    /// ```rust
+    /// # async fn run() -> Result<(), reqwest::Error> {
+    /// let body = reqwest::get("https://www.rust-lang.org").await?
+    ///     .text().await?;
+    /// # Ok(())
+    /// # }
+    /// ```
+    ///
+    /// # Errors
+    ///
+    /// This function fails if:
+    ///
+    /// - native TLS backend cannot be initialized
+    /// - supplied `Url` cannot be parsed
+    /// - there was an error while sending request
+    /// - redirect limit was exhausted
+    pub async fn get<T: IntoUrl>(url: T) -> crate::Result<Response> {
+        Client::builder().build()?.get(url).send().await
+    }
+}
diff --git a/vendored-deps/reqwest-wrap/src/response.rs b/vendored-deps/reqwest-wrap/src/response.rs
new file mode 100644
index 000000000..9c92cba53
--- /dev/null
+++ b/vendored-deps/reqwest-wrap/src/response.rs
@@ -0,0 +1,41 @@
+use url::Url;
+
+#[derive(Debug, Clone, PartialEq)]
+pub(crate) struct ResponseUrl(pub Url);
+
+/// Extension trait for http::response::Builder objects
+///
+/// Allows the user to add a `Url` to the http::Response
+pub trait ResponseBuilderExt {
+    /// A builder method for the `http::response::Builder` type that allows the user to add a `Url`
+    /// to the `http::Response`
+    fn url(self, url: Url) -> Self;
+}
+
+impl ResponseBuilderExt for http::response::Builder {
+    fn url(self, url: Url) -> Self {
+        self.extension(ResponseUrl(url))
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::{ResponseBuilderExt, ResponseUrl};
+    use http::response::Builder;
+    use url::Url;
+
+    #[test]
+    fn test_response_builder_ext() {
+        let url = Url::parse("http://example.com").unwrap();
+        let response = Builder::new()
+            .status(200)
+            .url(url.clone())
+            .body(())
+            .unwrap();
+
+        assert_eq!(
+            response.extensions().get::<ResponseUrl>(),
+            Some(&ResponseUrl(url))
+        );
+    }
+}
diff --git a/vendored-deps/reqwest-wrap/src/util.rs b/vendored-deps/reqwest-wrap/src/util.rs
new file mode 100644
index 000000000..b76a211d4
--- /dev/null
+++ b/vendored-deps/reqwest-wrap/src/util.rs
@@ -0,0 +1,30 @@
+use crate::header::{Entry, HeaderMap, OccupiedEntry};
+
+pub(crate) fn replace_headers(dst: &mut HeaderMap, src: HeaderMap) {
+    // IntoIter of HeaderMap yields (Option<HeaderName>, HeaderValue).
+    // The first time a name is yielded, it will be Some(name), and if
+    // there are more values with the same name, the next yield will be
+    // None.
+
+    let mut prev_entry: Option<OccupiedEntry<_>> = None;
+    for (key, value) in src {
+        match key {
+            Some(key) => match dst.entry(key) {
+                Entry::Occupied(mut e) => {
+                    e.insert(value);
+                    prev_entry = Some(e);
+                }
+                Entry::Vacant(e) => {
+                    let e = e.insert_entry(value);
+                    prev_entry = Some(e);
+                }
+            },
+            None => match prev_entry {
+                Some(ref mut entry) => {
+                    entry.append(value);
+                }
+                None => unreachable!("HeaderMap::into_iter yielded None first"),
+            },
+        }
+    }
+}
diff --git a/vendored-deps/reqwest-wrap/src/wasm/body.rs b/vendored-deps/reqwest-wrap/src/wasm/body.rs
new file mode 100644
index 000000000..9e925e9c5
--- /dev/null
+++ b/vendored-deps/reqwest-wrap/src/wasm/body.rs
@@ -0,0 +1,292 @@
+#![allow(clippy::unused_unit)]
+#[cfg(feature = "multipart")]
+use super::multipart::Form;
+/// dox
+use bytes::Bytes;
+use js_sys::Uint8Array;
+use std::fmt;
+use wasm_bindgen::JsValue;
+
+/// The body of a `Request`.
+///
+/// In most cases, this is not needed directly, as the
+/// [`RequestBuilder.body`][builder] method uses `Into<Body>`, which allows
+/// passing many things (like a string or vector of bytes).
+///
+/// [builder]: ./struct.RequestBuilder.html#method.body
+pub struct Body {
+    inner: Inner,
+}
+
+enum Inner {
+    Bytes(Bytes),
+    /// MultipartForm holds a multipart/form-data body.
+    #[cfg(feature = "multipart")]
+    MultipartForm(Form),
+    /// MultipartPart holds the body of a multipart/form-data part.
+    #[cfg(feature = "multipart")]
+    MultipartPart(Bytes),
+}
+
+impl Body {
+    /// Returns a reference to the internal data of the `Body`.
+    ///
+    /// `None` is returned, if the underlying data is a multipart form.
+    #[inline]
+    pub fn as_bytes(&self) -> Option<&[u8]> {
+        match &self.inner {
+            Inner::Bytes(bytes) => Some(bytes.as_ref()),
+            #[cfg(feature = "multipart")]
+            Inner::MultipartForm(_) => None,
+            #[cfg(feature = "multipart")]
+            Inner::MultipartPart(bytes) => Some(bytes.as_ref()),
+        }
+    }
+    pub(crate) fn to_js_value(&self) -> crate::Result<JsValue> {
+        match &self.inner {
+            Inner::Bytes(body_bytes) => {
+                let body_bytes: &[u8] = body_bytes.as_ref();
+                let body_uint8_array: Uint8Array = body_bytes.into();
+                let js_value: &JsValue = body_uint8_array.as_ref();
+                Ok(js_value.to_owned())
+            }
+            #[cfg(feature = "multipart")]
+            Inner::MultipartForm(form) => {
+                let form_data = form.to_form_data()?;
+                let js_value: &JsValue = form_data.as_ref();
+                Ok(js_value.to_owned())
+            }
+            #[cfg(feature = "multipart")]
+            Inner::MultipartPart(body_bytes) => {
+                let body_bytes: &[u8] = body_bytes.as_ref();
+                let body_uint8_array: Uint8Array = body_bytes.into();
+                let body_array = js_sys::Array::new();
+                body_array.push(&body_uint8_array);
+                let js_value: &JsValue = body_array.as_ref();
+                Ok(js_value.to_owned())
+            }
+        }
+    }
+
+    #[inline]
+    #[cfg(feature = "multipart")]
+    pub(crate) fn from_form(f: Form) -> Body {
+        Self {
+            inner: Inner::MultipartForm(f),
+        }
+    }
+
+    /// into_part turns a regular body into the body of a mutlipart/form-data part.
+    #[cfg(feature = "multipart")]
+    pub(crate) fn into_part(self) -> Body {
+        match self.inner {
+            Inner::Bytes(bytes) => Self {
+                inner: Inner::MultipartPart(bytes),
+            },
+            Inner::MultipartForm(form) => Self {
+                inner: Inner::MultipartForm(form),
+            },
+            Inner::MultipartPart(bytes) => Self {
+                inner: Inner::MultipartPart(bytes),
+            },
+        }
+    }
+
+    pub(crate) fn is_empty(&self) -> bool {
+        match &self.inner {
+            Inner::Bytes(bytes) => bytes.is_empty(),
+            #[cfg(feature = "multipart")]
+            Inner::MultipartForm(form) => form.is_empty(),
+            #[cfg(feature = "multipart")]
+            Inner::MultipartPart(bytes) => bytes.is_empty(),
+        }
+    }
+
+    pub(crate) fn try_clone(&self) -> Option<Body> {
+        match &self.inner {
+            Inner::Bytes(bytes) => Some(Self {
+                inner: Inner::Bytes(bytes.clone()),
+            }),
+            #[cfg(feature = "multipart")]
+            Inner::MultipartForm(_) => None,
+            #[cfg(feature = "multipart")]
+            Inner::MultipartPart(bytes) => Some(Self {
+                inner: Inner::MultipartPart(bytes.clone()),
+            }),
+        }
+    }
+}
+
+impl From<Bytes> for Body {
+    #[inline]
+    fn from(bytes: Bytes) -> Body {
+        Body {
+            inner: Inner::Bytes(bytes),
+        }
+    }
+}
+
+impl From<Vec<u8>> for Body {
+    #[inline]
+    fn from(vec: Vec<u8>) -> Body {
+        Body {
+            inner: Inner::Bytes(vec.into()),
+        }
+    }
+}
+
+impl From<&'static [u8]> for Body {
+    #[inline]
+    fn from(s: &'static [u8]) -> Body {
+        Body {
+            inner: Inner::Bytes(Bytes::from_static(s)),
+        }
+    }
+}
+
+impl From<String> for Body {
+    #[inline]
+    fn from(s: String) -> Body {
+        Body {
+            inner: Inner::Bytes(s.into()),
+        }
+    }
+}
+
+impl From<&'static str> for Body {
+    #[inline]
+    fn from(s: &'static str) -> Body {
+        s.as_bytes().into()
+    }
+}
+
+impl fmt::Debug for Body {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        f.debug_struct("Body").finish()
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use crate::Body;
+    use js_sys::Uint8Array;
+    use wasm_bindgen::prelude::*;
+    use wasm_bindgen_test::*;
+
+    wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
+
+    #[wasm_bindgen]
+    extern "C" {
+        // Use `js_namespace` here to bind `console.log(..)` instead of just
+        // `log(..)`
+        #[wasm_bindgen(js_namespace = console)]
+        fn log(s: String);
+    }
+
+    #[wasm_bindgen_test]
+    async fn test_body() {
+        let body = Body::from("TEST");
+        assert_eq!([84, 69, 83, 84], body.as_bytes().unwrap());
+    }
+
+    #[wasm_bindgen_test]
+    async fn test_body_js_static_str() {
+        let body_value = "TEST";
+        let body = Body::from(body_value);
+
+        let mut init = web_sys::RequestInit::new();
+        init.method("POST");
+        init.body(Some(
+            body.to_js_value()
+                .expect("could not convert body to JsValue")
+                .as_ref(),
+        ));
+
+        let js_req = web_sys::Request::new_with_str_and_init("", &init)
+            .expect("could not create JS request");
+        let text_promise = js_req.text().expect("could not get text promise");
+        let text = crate::wasm::promise::<JsValue>(text_promise)
+            .await
+            .expect("could not get request body as text");
+
+        assert_eq!(text.as_string().expect("text is not a string"), body_value);
+    }
+    #[wasm_bindgen_test]
+    async fn test_body_js_string() {
+        let body_value = "TEST".to_string();
+        let body = Body::from(body_value.clone());
+
+        let mut init = web_sys::RequestInit::new();
+        init.method("POST");
+        init.body(Some(
+            body.to_js_value()
+                .expect("could not convert body to JsValue")
+                .as_ref(),
+        ));
+
+        let js_req = web_sys::Request::new_with_str_and_init("", &init)
+            .expect("could not create JS request");
+        let text_promise = js_req.text().expect("could not get text promise");
+        let text = crate::wasm::promise::<JsValue>(text_promise)
+            .await
+            .expect("could not get request body as text");
+
+        assert_eq!(text.as_string().expect("text is not a string"), body_value);
+    }
+
+    #[wasm_bindgen_test]
+    async fn test_body_js_static_u8_slice() {
+        let body_value: &'static [u8] = b"\x00\x42";
+        let body = Body::from(body_value);
+
+        let mut init = web_sys::RequestInit::new();
+        init.method("POST");
+        init.body(Some(
+            body.to_js_value()
+                .expect("could not convert body to JsValue")
+                .as_ref(),
+        ));
+
+        let js_req = web_sys::Request::new_with_str_and_init("", &init)
+            .expect("could not create JS request");
+
+        let array_buffer_promise = js_req
+            .array_buffer()
+            .expect("could not get array_buffer promise");
+        let array_buffer = crate::wasm::promise::<JsValue>(array_buffer_promise)
+            .await
+            .expect("could not get request body as array buffer");
+
+        let v = Uint8Array::new(&array_buffer).to_vec();
+
+        assert_eq!(v, body_value);
+    }
+
+    #[wasm_bindgen_test]
+    async fn test_body_js_vec_u8() {
+        let body_value = vec![0u8, 42];
+        let body = Body::from(body_value.clone());
+
+        let mut init = web_sys::RequestInit::new();
+        init.method("POST");
+        init.body(Some(
+            body.to_js_value()
+                .expect("could not convert body to JsValue")
+                .as_ref(),
+        ));
+
+        let js_req = web_sys::Request::new_with_str_and_init("", &init)
+            .expect("could not create JS request");
+
+        let array_buffer_promise = js_req
+            .array_buffer()
+            .expect("could not get array_buffer promise");
+        let array_buffer = crate::wasm::promise::<JsValue>(array_buffer_promise)
+            .await
+            .expect("could not get request body as array buffer");
+
+        let v = Uint8Array::new(&array_buffer).to_vec();
+
+        assert_eq!(v, body_value);
+    }
+}
diff --git a/vendored-deps/reqwest-wrap/src/wasm/client.rs b/vendored-deps/reqwest-wrap/src/wasm/client.rs
new file mode 100644
index 000000000..ca1454c80
--- /dev/null
+++ b/vendored-deps/reqwest-wrap/src/wasm/client.rs
@@ -0,0 +1,420 @@
+#![allow(clippy::unused_unit)]
+use http::{HeaderMap, Method};
+use js_sys::{Promise, JSON};
+use std::rc::Rc;
+use std::time::Duration;
+use std::{fmt, future::Future, sync::Arc};
+use url::Url;
+use wasm_bindgen::prelude::{wasm_bindgen, Closure, UnwrapThrowExt as _};
+use wasm_bindgen::JsCast;
+
+use super::{Request, RequestBuilder, Response};
+use crate::IntoUrl;
+
+#[wasm_bindgen]
+extern "C" {
+    #[wasm_bindgen(js_name = fetch)]
+    fn fetch_with_request(input: &web_sys::Request) -> Promise;
+
+    #[wasm_bindgen(js_namespace = console, js_name = log)]
+    fn debug(s: &str);
+}
+
+fn js_fetch(req: &web_sys::Request) -> Promise {
+    use wasm_bindgen::{JsCast, JsValue};
+    let global = js_sys::global();
+
+    if let Ok(true) = js_sys::Reflect::has(&global, &JsValue::from_str("ServiceWorkerGlobalScope"))
+    {
+        global
+            .unchecked_into::<web_sys::ServiceWorkerGlobalScope>()
+            .fetch_with_request(req)
+    } else {
+        // browser
+        fetch_with_request(req)
+    }
+}
+
+/// dox
+#[derive(Clone)]
+pub struct Client {
+    config: Arc<Config>,
+    timeout: Option<Duration>,
+}
+
+/// dox
+pub struct ClientBuilder {
+    config: Config,
+    timeout: Option<Duration>,
+}
+
+impl Client {
+    /// dox
+    pub fn new() -> Self {
+        Client::builder().build().unwrap_throw()
+    }
+
+    /// dox
+    pub fn builder() -> ClientBuilder {
+        ClientBuilder::new()
+    }
+
+    /// Convenience method to make a `GET` request to a URL.
+    ///
+    /// # Errors
+    ///
+    /// This method fails whenever supplied `Url` cannot be parsed.
+    pub fn get<U: IntoUrl>(&self, url: U) -> RequestBuilder {
+        self.request(Method::GET, url)
+    }
+
+    /// Convenience method to make a `POST` request to a URL.
+    ///
+    /// # Errors
+    ///
+    /// This method fails whenever supplied `Url` cannot be parsed.
+    pub fn post<U: IntoUrl>(&self, url: U) -> RequestBuilder {
+        self.request(Method::POST, url)
+    }
+
+    /// Convenience method to make a `PUT` request to a URL.
+    ///
+    /// # Errors
+    ///
+    /// This method fails whenever supplied `Url` cannot be parsed.
+    pub fn put<U: IntoUrl>(&self, url: U) -> RequestBuilder {
+        self.request(Method::PUT, url)
+    }
+
+    /// Convenience method to make a `PATCH` request to a URL.
+    ///
+    /// # Errors
+    ///
+    /// This method fails whenever supplied `Url` cannot be parsed.
+    pub fn patch<U: IntoUrl>(&self, url: U) -> RequestBuilder {
+        self.request(Method::PATCH, url)
+    }
+
+    /// Convenience method to make a `DELETE` request to a URL.
+    ///
+    /// # Errors
+    ///
+    /// This method fails whenever supplied `Url` cannot be parsed.
+    pub fn delete<U: IntoUrl>(&self, url: U) -> RequestBuilder {
+        self.request(Method::DELETE, url)
+    }
+
+    /// Convenience method to make a `HEAD` request to a URL.
+    ///
+    /// # Errors
+    ///
+    /// This method fails whenever supplied `Url` cannot be parsed.
+    pub fn head<U: IntoUrl>(&self, url: U) -> RequestBuilder {
+        self.request(Method::HEAD, url)
+    }
+
+    /// Start building a `Request` with the `Method` and `Url`.
+    ///
+    /// Returns a `RequestBuilder`, which will allow setting headers and
+    /// request body before sending.
+    ///
+    /// # Errors
+    ///
+    /// This method fails whenever supplied `Url` cannot be parsed.
+    pub fn request<U: IntoUrl>(&self, method: Method, url: U) -> RequestBuilder {
+        let req = url.into_url().map(move |url| Request::new(method, url));
+        let builder = RequestBuilder::new(self.clone(), req);
+        if let Some(t) = self.timeout {
+            builder.timeout(t)
+        } else {
+            builder
+        }
+    }
+
+    /// Executes a `Request`.
+    ///
+    /// A `Request` can be built manually with `Request::new()` or obtained
+    /// from a RequestBuilder with `RequestBuilder::build()`.
+    ///
+    /// You should prefer to use the `RequestBuilder` and
+    /// `RequestBuilder::send()`.
+    ///
+    /// # Errors
+    ///
+    /// This method fails if there was an error while sending request,
+    /// redirect loop was detected or redirect limit was exhausted.
+    pub fn execute(
+        &self,
+        request: Request,
+    ) -> impl Future<Output = Result<Response, crate::Error>> {
+        self.execute_request(request)
+    }
+
+    // merge request headers with Client default_headers, prior to external http fetch
+    fn merge_headers(&self, req: &mut Request) {
+        use http::header::Entry;
+        let headers: &mut HeaderMap = req.headers_mut();
+        // insert default headers in the request headers
+        // without overwriting already appended headers.
+        for (key, value) in self.config.headers.iter() {
+            if let Entry::Vacant(entry) = headers.entry(key) {
+                entry.insert(value.clone());
+            }
+        }
+    }
+
+    pub(super) fn execute_request(
+        &self,
+        mut req: Request,
+    ) -> impl Future<Output = crate::Result<Response>> {
+        self.merge_headers(&mut req);
+        fetch(req)
+    }
+}
+
+impl Default for Client {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+impl fmt::Debug for Client {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        let mut builder = f.debug_struct("Client");
+        self.config.fmt_fields(&mut builder);
+        builder.finish()
+    }
+}
+
+impl fmt::Debug for ClientBuilder {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        let mut builder = f.debug_struct("ClientBuilder");
+        self.config.fmt_fields(&mut builder);
+        builder.finish()
+    }
+}
+
+async fn fetch(req: Request) -> crate::Result<Response> {
+    // Build the js Request
+    let mut init = web_sys::RequestInit::new();
+    let abort_controller = Rc::new(web_sys::AbortController::new().unwrap());
+    let abort_signal = Rc::clone(&abort_controller).signal();
+    let window = web_sys::window().expect("should have a window in this context");
+    init.method(req.method().as_str());
+
+    // convert HeaderMap to Headers
+    let js_headers = web_sys::Headers::new()
+        .map_err(crate::error::wasm)
+        .map_err(crate::error::builder)?;
+
+    for (name, value) in req.headers() {
+        js_headers
+            .append(
+                name.as_str(),
+                value.to_str().map_err(crate::error::builder)?,
+            )
+            .map_err(crate::error::wasm)
+            .map_err(crate::error::builder)?;
+    }
+    init.headers(&js_headers.into());
+
+    // When req.cors is true, do nothing because the default mode is 'cors'
+    if !req.cors {
+        init.mode(web_sys::RequestMode::NoCors);
+    }
+
+    if let Some(creds) = req.credentials {
+        init.credentials(creds);
+    }
+
+    if let Some(body) = req.body() {
+        if !body.is_empty() {
+            init.body(Some(body.to_js_value()?.as_ref()));
+        }
+    }
+
+    if let Some(duration) = req.timeout() {
+        let abort_request_cb = Closure::wrap(Box::new(move || {
+            abort_controller.abort();
+        }) as Box<dyn Fn()>);
+
+        init.signal(Some(&abort_signal));
+
+        window
+            .set_timeout_with_callback_and_timeout_and_arguments_0(
+                abort_request_cb.as_ref().unchecked_ref(),
+                duration.as_millis() as i32,
+            )
+            .expect("timeout was set");
+
+        abort_request_cb.forget();
+    }
+
+    let js_req = web_sys::Request::new_with_str_and_init(req.url().as_str(), &init)
+        .map_err(crate::error::wasm)
+        .map_err(crate::error::builder)?;
+
+    // Await the fetch() promise
+    let p = js_fetch(&js_req);
+    let js_resp = super::promise::<web_sys::Response>(p)
+        .await
+        .map_err(crate::error::request)?;
+
+    // Convert from the js Response
+    let mut resp = http::Response::builder().status(js_resp.status());
+
+    let url = Url::parse(&js_resp.url()).expect_throw("url parse");
+
+    let js_headers = js_resp.headers();
+    let js_iter = js_sys::try_iter(&js_headers)
+        .expect_throw("headers try_iter")
+        .expect_throw("headers have an iterator");
+
+    for item in js_iter {
+        let item = item.expect_throw("headers iterator doesn't throw");
+        let serialized_headers: String = JSON::stringify(&item)
+            .expect_throw("serialized headers")
+            .into();
+        let [name, value]: [String; 2] = serde_json::from_str(&serialized_headers)
+            .expect_throw("deserializable serialized headers");
+        resp = resp.header(&name, &value);
+    }
+
+    resp.body(js_resp)
+        .map(|resp| Response::new(resp, url))
+        .map_err(crate::error::request)
+}
+
+// ===== impl ClientBuilder =====
+
+impl ClientBuilder {
+    /// dox
+    pub fn new() -> Self {
+        ClientBuilder {
+            config: Config::default(),
+            timeout: Some(Duration::from_secs(30)),
+        }
+    }
+
+    /// Returns a 'Client' that uses this ClientBuilder configuration
+    pub fn build(mut self) -> Result<Client, crate::Error> {
+        let config = std::mem::take(&mut self.config);
+        Ok(Client {
+            config: Arc::new(config),
+            timeout: self.timeout,
+        })
+    }
+
+    /// Sets the default headers for every request
+    pub fn default_headers(mut self, headers: HeaderMap) -> ClientBuilder {
+        for (key, value) in headers.iter() {
+            self.config.headers.insert(key, value.clone());
+        }
+        self
+    }
+
+    /// Set a timeout for connect, read and write operations of a `Client`.
+    ///
+    /// Default is 30 seconds.
+    pub fn timeout(mut self, timeout: Duration) -> ClientBuilder {
+        self.timeout = Some(timeout);
+        self
+    }
+}
+
+impl Default for ClientBuilder {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+#[derive(Clone, Debug)]
+struct Config {
+    headers: HeaderMap,
+}
+
+impl Default for Config {
+    fn default() -> Config {
+        Config {
+            headers: HeaderMap::new(),
+        }
+    }
+}
+
+impl Config {
+    fn fmt_fields(&self, f: &mut fmt::DebugStruct<'_, '_>) {
+        f.field("default_headers", &self.headers);
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use wasm_bindgen_test::*;
+
+    wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
+
+    #[wasm_bindgen_test]
+    async fn default_headers() {
+        use crate::header::{HeaderMap, HeaderValue, CONTENT_TYPE};
+
+        let mut headers = HeaderMap::new();
+        headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
+        headers.insert("x-custom", HeaderValue::from_static("flibbertigibbet"));
+        let client = crate::Client::builder()
+            .default_headers(headers)
+            .build()
+            .expect("client");
+        let mut req = client
+            .get("https://www.example.com")
+            .build()
+            .expect("request");
+        // merge headers as if client were about to issue fetch
+        client.merge_headers(&mut req);
+
+        let test_headers = req.headers();
+        assert!(test_headers.get(CONTENT_TYPE).is_some(), "content-type");
+        assert!(test_headers.get("x-custom").is_some(), "custom header");
+        assert!(test_headers.get("accept").is_none(), "no accept header");
+    }
+
+    #[wasm_bindgen_test]
+    async fn default_headers_clone() {
+        use crate::header::{HeaderMap, HeaderValue, CONTENT_TYPE};
+
+        let mut headers = HeaderMap::new();
+        headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
+        headers.insert("x-custom", HeaderValue::from_static("flibbertigibbet"));
+        let client = crate::Client::builder()
+            .default_headers(headers)
+            .build()
+            .expect("client");
+
+        let mut req = client
+            .get("https://www.example.com")
+            .header(CONTENT_TYPE, "text/plain")
+            .build()
+            .expect("request");
+        client.merge_headers(&mut req);
+        let headers1 = req.headers();
+
+        // confirm that request headers override defaults
+        assert_eq!(
+            headers1.get(CONTENT_TYPE).unwrap(),
+            "text/plain",
+            "request headers override defaults"
+        );
+
+        // confirm that request headers don't change client defaults
+        let mut req2 = client
+            .get("https://www.example.com/x")
+            .build()
+            .expect("req 2");
+        client.merge_headers(&mut req2);
+        let headers2 = req2.headers();
+        assert_eq!(
+            headers2.get(CONTENT_TYPE).unwrap(),
+            "application/json",
+            "request headers don't change client defaults"
+        );
+    }
+}
diff --git a/vendored-deps/reqwest-wrap/src/wasm/mod.rs b/vendored-deps/reqwest-wrap/src/wasm/mod.rs
new file mode 100644
index 000000000..249b4a284
--- /dev/null
+++ b/vendored-deps/reqwest-wrap/src/wasm/mod.rs
@@ -0,0 +1,27 @@
+use wasm_bindgen::JsCast;
+
+mod body;
+mod client;
+/// TODO
+#[cfg(feature = "multipart")]
+pub mod multipart;
+mod request;
+mod response;
+
+pub use self::body::Body;
+pub use self::client::{Client, ClientBuilder};
+pub use self::request::{Request, RequestBuilder};
+pub use self::response::Response;
+
+async fn promise<T>(promise: js_sys::Promise) -> Result<T, crate::error::BoxError>
+where
+    T: JsCast,
+{
+    use wasm_bindgen_futures::JsFuture;
+
+    let js_val = JsFuture::from(promise).await.map_err(crate::error::wasm)?;
+
+    js_val
+        .dyn_into::<T>()
+        .map_err(|_js_val| "promise resolved to unexpected type".into())
+}
diff --git a/vendored-deps/reqwest-wrap/src/wasm/multipart.rs b/vendored-deps/reqwest-wrap/src/wasm/multipart.rs
new file mode 100644
index 000000000..909875c0a
--- /dev/null
+++ b/vendored-deps/reqwest-wrap/src/wasm/multipart.rs
@@ -0,0 +1,360 @@
+//! multipart/form-data
+use std::borrow::Cow;
+use std::fmt;
+
+use http::HeaderMap;
+use mime_guess::Mime;
+use web_sys::FormData;
+
+use super::Body;
+
+/// An async multipart/form-data request.
+pub struct Form {
+    inner: FormParts<Part>,
+}
+
+impl Form {
+    pub(crate) fn is_empty(&self) -> bool {
+        self.inner.fields.is_empty()
+    }
+}
+
+/// A field in a multipart form.
+pub struct Part {
+    meta: PartMetadata,
+    value: Body,
+}
+
+pub(crate) struct FormParts<P> {
+    pub(crate) fields: Vec<(Cow<'static, str>, P)>,
+}
+
+pub(crate) struct PartMetadata {
+    mime: Option<Mime>,
+    file_name: Option<Cow<'static, str>>,
+    pub(crate) headers: HeaderMap,
+}
+
+pub(crate) trait PartProps {
+    fn metadata(&self) -> &PartMetadata;
+}
+
+// ===== impl Form =====
+
+impl Default for Form {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+impl Form {
+    /// Creates a new async Form without any content.
+    pub fn new() -> Form {
+        Form {
+            inner: FormParts::new(),
+        }
+    }
+
+    /// Add a data field with supplied name and value.
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// let form = reqwest::multipart::Form::new()
+    ///     .text("username", "seanmonstar")
+    ///     .text("password", "secret");
+    /// ```
+    pub fn text<T, U>(self, name: T, value: U) -> Form
+    where
+        T: Into<Cow<'static, str>>,
+        U: Into<Cow<'static, str>>,
+    {
+        self.part(name, Part::text(value))
+    }
+
+    /// Adds a customized Part.
+    pub fn part<T>(self, name: T, part: Part) -> Form
+    where
+        T: Into<Cow<'static, str>>,
+    {
+        self.with_inner(move |inner| inner.part(name, part))
+    }
+
+    fn with_inner<F>(self, func: F) -> Self
+    where
+        F: FnOnce(FormParts<Part>) -> FormParts<Part>,
+    {
+        Form {
+            inner: func(self.inner),
+        }
+    }
+
+    pub(crate) fn to_form_data(&self) -> crate::Result<FormData> {
+        let form = FormData::new()
+            .map_err(crate::error::wasm)
+            .map_err(crate::error::builder)?;
+
+        for (name, part) in self.inner.fields.iter() {
+            let blob = part.blob()?;
+
+            if let Some(file_name) = &part.metadata().file_name {
+                form.append_with_blob_and_filename(name, &blob, &file_name)
+            } else {
+                form.append_with_blob(name, &blob)
+            }
+            .map_err(crate::error::wasm)
+            .map_err(crate::error::builder)?;
+        }
+        Ok(form)
+    }
+}
+
+impl fmt::Debug for Form {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        self.inner.fmt_fields("Form", f)
+    }
+}
+
+// ===== impl Part =====
+
+impl Part {
+    /// Makes a text parameter.
+    pub fn text<T>(value: T) -> Part
+    where
+        T: Into<Cow<'static, str>>,
+    {
+        let body = match value.into() {
+            Cow::Borrowed(slice) => Body::from(slice),
+            Cow::Owned(string) => Body::from(string),
+        };
+        Part::new(body)
+    }
+
+    /// Makes a new parameter from arbitrary bytes.
+    pub fn bytes<T>(value: T) -> Part
+    where
+        T: Into<Cow<'static, [u8]>>,
+    {
+        let body = match value.into() {
+            Cow::Borrowed(slice) => Body::from(slice),
+            Cow::Owned(vec) => Body::from(vec),
+        };
+        Part::new(body)
+    }
+
+    /// Makes a new parameter from an arbitrary stream.
+    pub fn stream<T: Into<Body>>(value: T) -> Part {
+        Part::new(value.into())
+    }
+
+    fn new(value: Body) -> Part {
+        Part {
+            meta: PartMetadata::new(),
+            value: value.into_part(),
+        }
+    }
+
+    /// Tries to set the mime of this part.
+    pub fn mime_str(self, mime: &str) -> crate::Result<Part> {
+        Ok(self.mime(mime.parse().map_err(crate::error::builder)?))
+    }
+
+    // Re-export when mime 0.4 is available, with split MediaType/MediaRange.
+    fn mime(self, mime: Mime) -> Part {
+        self.with_inner(move |inner| inner.mime(mime))
+    }
+
+    /// Sets the filename, builder style.
+    pub fn file_name<T>(self, filename: T) -> Part
+    where
+        T: Into<Cow<'static, str>>,
+    {
+        self.with_inner(move |inner| inner.file_name(filename))
+    }
+
+    fn with_inner<F>(self, func: F) -> Self
+    where
+        F: FnOnce(PartMetadata) -> PartMetadata,
+    {
+        Part {
+            meta: func(self.meta),
+            value: self.value,
+        }
+    }
+
+    fn blob(&self) -> crate::Result<web_sys::Blob> {
+        use web_sys::Blob;
+        use web_sys::BlobPropertyBag;
+        let mut properties = BlobPropertyBag::new();
+        if let Some(mime) = &self.meta.mime {
+            properties.type_(mime.as_ref());
+        }
+
+        // BUG: the return value of to_js_value() is not valid if
+        // it is a MultipartForm variant.
+        let js_value = self.value.to_js_value()?;
+        Blob::new_with_u8_array_sequence_and_options(&js_value, &properties)
+            .map_err(crate::error::wasm)
+            .map_err(crate::error::builder)
+    }
+}
+
+impl fmt::Debug for Part {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        let mut dbg = f.debug_struct("Part");
+        dbg.field("value", &self.value);
+        self.meta.fmt_fields(&mut dbg);
+        dbg.finish()
+    }
+}
+
+impl PartProps for Part {
+    fn metadata(&self) -> &PartMetadata {
+        &self.meta
+    }
+}
+
+// ===== impl FormParts =====
+
+impl<P: PartProps> FormParts<P> {
+    pub(crate) fn new() -> Self {
+        FormParts { fields: Vec::new() }
+    }
+
+    /// Adds a customized Part.
+    pub(crate) fn part<T>(mut self, name: T, part: P) -> Self
+    where
+        T: Into<Cow<'static, str>>,
+    {
+        self.fields.push((name.into(), part));
+        self
+    }
+}
+
+impl<P: fmt::Debug> FormParts<P> {
+    pub(crate) fn fmt_fields(&self, ty_name: &'static str, f: &mut fmt::Formatter) -> fmt::Result {
+        f.debug_struct(ty_name)
+            .field("parts", &self.fields)
+            .finish()
+    }
+}
+
+// ===== impl PartMetadata =====
+
+impl PartMetadata {
+    pub(crate) fn new() -> Self {
+        PartMetadata {
+            mime: None,
+            file_name: None,
+            headers: HeaderMap::default(),
+        }
+    }
+
+    pub(crate) fn mime(mut self, mime: Mime) -> Self {
+        self.mime = Some(mime);
+        self
+    }
+
+    pub(crate) fn file_name<T>(mut self, filename: T) -> Self
+    where
+        T: Into<Cow<'static, str>>,
+    {
+        self.file_name = Some(filename.into());
+        self
+    }
+}
+
+impl PartMetadata {
+    pub(crate) fn fmt_fields<'f, 'fa, 'fb>(
+        &self,
+        debug_struct: &'f mut fmt::DebugStruct<'fa, 'fb>,
+    ) -> &'f mut fmt::DebugStruct<'fa, 'fb> {
+        debug_struct
+            .field("mime", &self.mime)
+            .field("file_name", &self.file_name)
+            .field("headers", &self.headers)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+
+    use wasm_bindgen_test::*;
+
+    wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
+
+    #[wasm_bindgen_test]
+    async fn test_multipart_js() {
+        use super::{Form, Part};
+        use js_sys::Uint8Array;
+        use wasm_bindgen::JsValue;
+        use web_sys::{File, FormData};
+
+        let text_file_name = "test.txt";
+        let text_file_type = "text/plain";
+        let text_content = "TEST";
+        let text_part = Part::text(text_content)
+            .file_name(text_file_name)
+            .mime_str(text_file_type)
+            .expect("invalid mime type");
+
+        let binary_file_name = "binary.bin";
+        let binary_file_type = "application/octet-stream";
+        let binary_content = vec![0u8, 42];
+        let binary_part = Part::bytes(binary_content.clone())
+            .file_name(binary_file_name)
+            .mime_str(binary_file_type)
+            .expect("invalid mime type");
+
+        let text_name = "text part";
+        let binary_name = "binary part";
+        let form = Form::new()
+            .part(text_name, text_part)
+            .part(binary_name, binary_part);
+
+        let mut init = web_sys::RequestInit::new();
+        init.method("POST");
+        init.body(Some(
+            form.to_form_data()
+                .expect("could not convert to FormData")
+                .as_ref(),
+        ));
+
+        let js_req = web_sys::Request::new_with_str_and_init("", &init)
+            .expect("could not create JS request");
+
+        let form_data_promise = js_req.form_data().expect("could not get form_data promise");
+
+        let form_data = crate::wasm::promise::<FormData>(form_data_promise)
+            .await
+            .expect("could not get body as form data");
+
+        // check text part
+        let text_file = File::from(form_data.get(text_name));
+        assert_eq!(text_file.name(), text_file_name);
+        assert_eq!(text_file.type_(), text_file_type);
+
+        let text_promise = text_file.text();
+        let text = crate::wasm::promise::<JsValue>(text_promise)
+            .await
+            .expect("could not get text body as text");
+        assert_eq!(
+            text.as_string().expect("text is not a string"),
+            text_content
+        );
+
+        // check binary part
+        let binary_file = File::from(form_data.get(binary_name));
+        assert_eq!(binary_file.name(), binary_file_name);
+        assert_eq!(binary_file.type_(), binary_file_type);
+
+        let binary_array_buffer_promise = binary_file.array_buffer();
+        let array_buffer = crate::wasm::promise::<JsValue>(binary_array_buffer_promise)
+            .await
+            .expect("could not get request body as array buffer");
+
+        let binary = Uint8Array::new(&array_buffer).to_vec();
+
+        assert_eq!(binary, binary_content);
+    }
+}
diff --git a/vendored-deps/reqwest-wrap/src/wasm/request.rs b/vendored-deps/reqwest-wrap/src/wasm/request.rs
new file mode 100644
index 000000000..a1c1d6e10
--- /dev/null
+++ b/vendored-deps/reqwest-wrap/src/wasm/request.rs
@@ -0,0 +1,498 @@
+use std::convert::TryFrom;
+use std::fmt;
+use std::time::Duration;
+
+use bytes::Bytes;
+use http::{request::Parts, Method, Request as HttpRequest};
+use serde::Serialize;
+#[cfg(feature = "json")]
+use serde_json;
+use url::Url;
+use web_sys::RequestCredentials;
+
+use super::{Body, Client, Response};
+use crate::header::{HeaderMap, HeaderName, HeaderValue, CONTENT_TYPE};
+
+/// A request which can be executed with `Client::execute()`.
+pub struct Request {
+    method: Method,
+    url: Url,
+    headers: HeaderMap,
+    body: Option<Body>,
+    pub(super) cors: bool,
+    pub(super) credentials: Option<RequestCredentials>,
+    timeout: Option<Duration>,
+}
+
+/// A builder to construct the properties of a `Request`.
+pub struct RequestBuilder {
+    client: Client,
+    request: crate::Result<Request>,
+}
+
+impl Request {
+    /// Constructs a new request.
+    #[inline]
+    pub fn new(method: Method, url: Url) -> Self {
+        Request {
+            method,
+            url,
+            headers: HeaderMap::new(),
+            body: None,
+            cors: true,
+            credentials: None,
+            timeout: None,
+        }
+    }
+
+    /// Get the method.
+    #[inline]
+    pub fn method(&self) -> &Method {
+        &self.method
+    }
+
+    /// Get a mutable reference to the method.
+    #[inline]
+    pub fn method_mut(&mut self) -> &mut Method {
+        &mut self.method
+    }
+
+    /// Get the url.
+    #[inline]
+    pub fn url(&self) -> &Url {
+        &self.url
+    }
+
+    /// Get a mutable reference to the url.
+    #[inline]
+    pub fn url_mut(&mut self) -> &mut Url {
+        &mut self.url
+    }
+
+    /// Get the headers.
+    #[inline]
+    pub fn headers(&self) -> &HeaderMap {
+        &self.headers
+    }
+
+    /// Get a mutable reference to the headers.
+    #[inline]
+    pub fn headers_mut(&mut self) -> &mut HeaderMap {
+        &mut self.headers
+    }
+
+    /// Get the body.
+    #[inline]
+    pub fn body(&self) -> Option<&Body> {
+        self.body.as_ref()
+    }
+
+    /// Get a mutable reference to the body.
+    #[inline]
+    pub fn body_mut(&mut self) -> &mut Option<Body> {
+        &mut self.body
+    }
+
+    /// Get the timeout.
+    #[inline]
+    pub fn timeout(&self) -> Option<&Duration> {
+        self.timeout.as_ref()
+    }
+
+    /// Get a mutable reference to the timeout.
+    #[inline]
+    pub fn timeout_mut(&mut self) -> &mut Option<Duration> {
+        &mut self.timeout
+    }
+
+    /// Attempts to clone the `Request`.
+    ///
+    /// None is returned if a body is which can not be cloned.
+    pub fn try_clone(&self) -> Option<Request> {
+        let body = match self.body.as_ref() {
+            Some(body) => Some(body.try_clone()?),
+            None => None,
+        };
+
+        Some(Self {
+            method: self.method.clone(),
+            url: self.url.clone(),
+            headers: self.headers.clone(),
+            body,
+            cors: self.cors,
+            credentials: self.credentials,
+            timeout: self.timeout,
+        })
+    }
+}
+
+impl RequestBuilder {
+    pub(super) fn new(client: Client, request: crate::Result<Request>) -> RequestBuilder {
+        RequestBuilder { client, request }
+    }
+
+    /// Modify the query string of the URL.
+    ///
+    /// Modifies the URL of this request, adding the parameters provided.
+    /// This method appends and does not overwrite. This means that it can
+    /// be called multiple times and that existing query parameters are not
+    /// overwritten if the same key is used. The key will simply show up
+    /// twice in the query string.
+    /// Calling `.query([("foo", "a"), ("foo", "b")])` gives `"foo=a&foo=b"`.
+    ///
+    /// # Note
+    /// This method does not support serializing a single key-value
+    /// pair. Instead of using `.query(("key", "val"))`, use a sequence, such
+    /// as `.query(&[("key", "val")])`. It's also possible to serialize structs
+    /// and maps into a key-value pair.
+    ///
+    /// # Errors
+    /// This method will fail if the object you provide cannot be serialized
+    /// into a query string.
+    pub fn query<T: Serialize + ?Sized>(mut self, query: &T) -> RequestBuilder {
+        let mut error = None;
+        if let Ok(ref mut req) = self.request {
+            let url = req.url_mut();
+            let mut pairs = url.query_pairs_mut();
+            let serializer = serde_urlencoded::Serializer::new(&mut pairs);
+
+            if let Err(err) = query.serialize(serializer) {
+                error = Some(crate::error::builder(err));
+            }
+        }
+        if let Ok(ref mut req) = self.request {
+            if let Some("") = req.url().query() {
+                req.url_mut().set_query(None);
+            }
+        }
+        if let Some(err) = error {
+            self.request = Err(err);
+        }
+        self
+    }
+
+    /// Send a form body.
+    ///
+    /// Sets the body to the url encoded serialization of the passed value,
+    /// and also sets the `Content-Type: application/x-www-form-urlencoded`
+    /// header.
+    ///
+    /// # Errors
+    ///
+    /// This method fails if the passed value cannot be serialized into
+    /// url encoded format
+    pub fn form<T: Serialize + ?Sized>(mut self, form: &T) -> RequestBuilder {
+        let mut error = None;
+        if let Ok(ref mut req) = self.request {
+            match serde_urlencoded::to_string(form) {
+                Ok(body) => {
+                    req.headers_mut().insert(
+                        CONTENT_TYPE,
+                        HeaderValue::from_static("application/x-www-form-urlencoded"),
+                    );
+                    *req.body_mut() = Some(body.into());
+                }
+                Err(err) => error = Some(crate::error::builder(err)),
+            }
+        }
+        if let Some(err) = error {
+            self.request = Err(err);
+        }
+        self
+    }
+
+    #[cfg(feature = "json")]
+    #[cfg_attr(docsrs, doc(cfg(feature = "json")))]
+    /// Set the request json
+    pub fn json<T: Serialize + ?Sized>(mut self, json: &T) -> RequestBuilder {
+        let mut error = None;
+        if let Ok(ref mut req) = self.request {
+            match serde_json::to_vec(json) {
+                Ok(body) => {
+                    req.headers_mut()
+                        .insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
+                    *req.body_mut() = Some(body.into());
+                }
+                Err(err) => error = Some(crate::error::builder(err)),
+            }
+        }
+        if let Some(err) = error {
+            self.request = Err(err);
+        }
+        self
+    }
+
+    /// Enable HTTP bearer authentication.
+    pub fn bearer_auth<T>(self, token: T) -> RequestBuilder
+    where
+        T: fmt::Display,
+    {
+        let header_value = format!("Bearer {}", token);
+        self.header(crate::header::AUTHORIZATION, header_value)
+    }
+
+    /// Enables a request timeout.
+    ///
+    /// The timeout is applied from when the request starts connecting until the
+    /// response body has finished. It affects only this request and overrides
+    /// the timeout configured using `ClientBuilder::timeout()`.
+    pub fn timeout(mut self, timeout: Duration) -> RequestBuilder {
+        if let Ok(ref mut req) = self.request {
+            *req.timeout_mut() = Some(timeout);
+        }
+        self
+    }
+
+    /// Set the request body.
+    pub fn body<T: Into<Body>>(mut self, body: T) -> RequestBuilder {
+        if let Ok(ref mut req) = self.request {
+            req.body = Some(body.into());
+        }
+        self
+    }
+
+    /// TODO
+    #[cfg(feature = "multipart")]
+    #[cfg_attr(docsrs, doc(cfg(feature = "multipart")))]
+    pub fn multipart(mut self, multipart: super::multipart::Form) -> RequestBuilder {
+        if let Ok(ref mut req) = self.request {
+            *req.body_mut() = Some(Body::from_form(multipart))
+        }
+        self
+    }
+
+    /// Add a `Header` to this Request.
+    pub fn header<K, V>(mut self, key: K, value: V) -> RequestBuilder
+    where
+        HeaderName: TryFrom<K>,
+        <HeaderName as TryFrom<K>>::Error: Into<http::Error>,
+        HeaderValue: TryFrom<V>,
+        <HeaderValue as TryFrom<V>>::Error: Into<http::Error>,
+    {
+        let mut error = None;
+        if let Ok(ref mut req) = self.request {
+            match <HeaderName as TryFrom<K>>::try_from(key) {
+                Ok(key) => match <HeaderValue as TryFrom<V>>::try_from(value) {
+                    Ok(value) => {
+                        req.headers_mut().append(key, value);
+                    }
+                    Err(e) => error = Some(crate::error::builder(e.into())),
+                },
+                Err(e) => error = Some(crate::error::builder(e.into())),
+            };
+        }
+        if let Some(err) = error {
+            self.request = Err(err);
+        }
+        self
+    }
+
+    /// Add a set of Headers to the existing ones on this Request.
+    ///
+    /// The headers will be merged in to any already set.
+    pub fn headers(mut self, headers: crate::header::HeaderMap) -> RequestBuilder {
+        if let Ok(ref mut req) = self.request {
+            crate::util::replace_headers(req.headers_mut(), headers);
+        }
+        self
+    }
+
+    /// Disable CORS on fetching the request.
+    ///
+    /// # WASM
+    ///
+    /// This option is only effective with WebAssembly target.
+    ///
+    /// The [request mode][mdn] will be set to 'no-cors'.
+    ///
+    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/Request/mode
+    pub fn fetch_mode_no_cors(mut self) -> RequestBuilder {
+        if let Ok(ref mut req) = self.request {
+            req.cors = false;
+        }
+        self
+    }
+
+    /// Set fetch credentials to 'same-origin'
+    ///
+    /// # WASM
+    ///
+    /// This option is only effective with WebAssembly target.
+    ///
+    /// The [request credentials][mdn] will be set to 'same-origin'.
+    ///
+    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/Request/credentials
+    pub fn fetch_credentials_same_origin(mut self) -> RequestBuilder {
+        if let Ok(ref mut req) = self.request {
+            req.credentials = Some(RequestCredentials::SameOrigin);
+        }
+        self
+    }
+
+    /// Set fetch credentials to 'include'
+    ///
+    /// # WASM
+    ///
+    /// This option is only effective with WebAssembly target.
+    ///
+    /// The [request credentials][mdn] will be set to 'include'.
+    ///
+    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/Request/credentials
+    pub fn fetch_credentials_include(mut self) -> RequestBuilder {
+        if let Ok(ref mut req) = self.request {
+            req.credentials = Some(RequestCredentials::Include);
+        }
+        self
+    }
+
+    /// Set fetch credentials to 'omit'
+    ///
+    /// # WASM
+    ///
+    /// This option is only effective with WebAssembly target.
+    ///
+    /// The [request credentials][mdn] will be set to 'omit'.
+    ///
+    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/Request/credentials
+    pub fn fetch_credentials_omit(mut self) -> RequestBuilder {
+        if let Ok(ref mut req) = self.request {
+            req.credentials = Some(RequestCredentials::Omit);
+        }
+        self
+    }
+
+    /// Build a `Request`, which can be inspected, modified and executed with
+    /// `Client::execute()`.
+    pub fn build(self) -> crate::Result<Request> {
+        self.request
+    }
+
+    /// Constructs the Request and sends it to the target URL, returning a
+    /// future Response.
+    ///
+    /// # Errors
+    ///
+    /// This method fails if there was an error while sending request.
+    ///
+    /// # Example
+    ///
+    /// ```no_run
+    /// # use reqwest::Error;
+    /// #
+    /// # async fn run() -> Result<(), Error> {
+    /// let response = reqwest::Client::new()
+    ///     .get("https://hyper.rs")
+    ///     .send()
+    ///     .await?;
+    /// # Ok(())
+    /// # }
+    /// ```
+    pub async fn send(self) -> crate::Result<Response> {
+        let req = self.request?;
+        self.client.execute_request(req).await
+    }
+
+    /// Attempt to clone the RequestBuilder.
+    ///
+    /// `None` is returned if the RequestBuilder can not be cloned.
+    ///
+    /// # Examples
+    ///
+    /// ```no_run
+    /// # use reqwest::Error;
+    /// #
+    /// # fn run() -> Result<(), Error> {
+    /// let client = reqwest::Client::new();
+    /// let builder = client.post("http://httpbin.org/post")
+    ///     .body("from a &str!");
+    /// let clone = builder.try_clone();
+    /// assert!(clone.is_some());
+    /// # Ok(())
+    /// # }
+    /// ```
+    pub fn try_clone(&self) -> Option<RequestBuilder> {
+        self.request
+            .as_ref()
+            .ok()
+            .and_then(|req| req.try_clone())
+            .map(|req| RequestBuilder {
+                client: self.client.clone(),
+                request: Ok(req),
+            })
+    }
+}
+
+impl fmt::Debug for Request {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        fmt_request_fields(&mut f.debug_struct("Request"), self).finish()
+    }
+}
+
+impl fmt::Debug for RequestBuilder {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        let mut builder = f.debug_struct("RequestBuilder");
+        match self.request {
+            Ok(ref req) => fmt_request_fields(&mut builder, req).finish(),
+            Err(ref err) => builder.field("error", err).finish(),
+        }
+    }
+}
+
+fn fmt_request_fields<'a, 'b>(
+    f: &'a mut fmt::DebugStruct<'a, 'b>,
+    req: &Request,
+) -> &'a mut fmt::DebugStruct<'a, 'b> {
+    f.field("method", &req.method)
+        .field("url", &req.url)
+        .field("headers", &req.headers)
+}
+
+impl<T> TryFrom<HttpRequest<T>> for Request
+where
+    T: Into<Body>,
+{
+    type Error = crate::Error;
+
+    fn try_from(req: HttpRequest<T>) -> crate::Result<Self> {
+        let (parts, body) = req.into_parts();
+        let Parts {
+            method,
+            uri,
+            headers,
+            ..
+        } = parts;
+        let url = Url::parse(&uri.to_string()).map_err(crate::error::builder)?;
+        Ok(Request {
+            method,
+            url,
+            headers,
+            body: Some(body.into()),
+            cors: true,
+            credentials: None,
+            timeout: None,
+        })
+    }
+}
+
+impl TryFrom<Request> for HttpRequest<Body> {
+    type Error = crate::Error;
+
+    fn try_from(req: Request) -> crate::Result<Self> {
+        let Request {
+            method,
+            url,
+            headers,
+            body,
+            ..
+        } = req;
+
+        let mut req = HttpRequest::builder()
+            .method(method)
+            .uri(url.as_str())
+            .body(body.unwrap_or_else(|| Body::from(Bytes::default())))
+            .map_err(crate::error::builder)?;
+
+        *req.headers_mut() = headers;
+        Ok(req)
+    }
+}
diff --git a/vendored-deps/reqwest-wrap/src/wasm/response.rs b/vendored-deps/reqwest-wrap/src/wasm/response.rs
new file mode 100644
index 000000000..44041e274
--- /dev/null
+++ b/vendored-deps/reqwest-wrap/src/wasm/response.rs
@@ -0,0 +1,152 @@
+use std::fmt;
+
+use bytes::Bytes;
+use http::{HeaderMap, StatusCode};
+use js_sys::Uint8Array;
+use url::Url;
+
+#[cfg(feature = "json")]
+use serde::de::DeserializeOwned;
+
+/// A Response to a submitted `Request`.
+pub struct Response {
+    http: http::Response<web_sys::Response>,
+    // Boxed to save space (11 words to 1 word), and it's not accessed
+    // frequently internally.
+    url: Box<Url>,
+}
+
+impl Response {
+    pub(super) fn new(res: http::Response<web_sys::Response>, url: Url) -> Response {
+        Response {
+            http: res,
+            url: Box::new(url),
+        }
+    }
+
+    /// Get the `StatusCode` of this `Response`.
+    #[inline]
+    pub fn status(&self) -> StatusCode {
+        self.http.status()
+    }
+
+    /// Get the `Headers` of this `Response`.
+    #[inline]
+    pub fn headers(&self) -> &HeaderMap {
+        self.http.headers()
+    }
+
+    /// Get a mutable reference to the `Headers` of this `Response`.
+    #[inline]
+    pub fn headers_mut(&mut self) -> &mut HeaderMap {
+        self.http.headers_mut()
+    }
+
+    /// Get the content-length of this response, if known.
+    ///
+    /// Reasons it may not be known:
+    ///
+    /// - The server didn't send a `content-length` header.
+    /// - The response is compressed and automatically decoded (thus changing
+    ///  the actual decoded length).
+    pub fn content_length(&self) -> Option<u64> {
+        self.headers()
+            .get(http::header::CONTENT_LENGTH)?
+            .to_str()
+            .ok()?
+            .parse()
+            .ok()
+    }
+
+    /// Get the final `Url` of this `Response`.
+    #[inline]
+    pub fn url(&self) -> &Url {
+        &self.url
+    }
+
+    /* It might not be possible to detect this in JS?
+    /// Get the HTTP `Version` of this `Response`.
+    #[inline]
+    pub fn version(&self) -> Version {
+        self.http.version()
+    }
+    */
+
+    /// Try to deserialize the response body as JSON.
+    #[cfg(feature = "json")]
+    #[cfg_attr(docsrs, doc(cfg(feature = "json")))]
+    pub async fn json<T: DeserializeOwned>(self) -> crate::Result<T> {
+        let full = self.bytes().await?;
+
+        serde_json::from_slice(&full).map_err(crate::error::decode)
+    }
+
+    /// Get the response text.
+    pub async fn text(self) -> crate::Result<String> {
+        let p = self
+            .http
+            .body()
+            .text()
+            .map_err(crate::error::wasm)
+            .map_err(crate::error::decode)?;
+        let js_val = super::promise::<wasm_bindgen::JsValue>(p)
+            .await
+            .map_err(crate::error::decode)?;
+        if let Some(s) = js_val.as_string() {
+            Ok(s)
+        } else {
+            Err(crate::error::decode("response.text isn't string"))
+        }
+    }
+
+    /// Get the response as bytes
+    pub async fn bytes(self) -> crate::Result<Bytes> {
+        let p = self
+            .http
+            .body()
+            .array_buffer()
+            .map_err(crate::error::wasm)
+            .map_err(crate::error::decode)?;
+
+        let buf_js = super::promise::<wasm_bindgen::JsValue>(p)
+            .await
+            .map_err(crate::error::decode)?;
+
+        let buffer = Uint8Array::new(&buf_js);
+        let mut bytes = vec![0; buffer.length() as usize];
+        buffer.copy_to(&mut bytes);
+        Ok(bytes.into())
+    }
+
+    // util methods
+
+    /// Turn a response into an error if the server returned an error.
+    pub fn error_for_status(self) -> crate::Result<Self> {
+        let status = self.status();
+        if status.is_client_error() || status.is_server_error() {
+            Err(crate::error::status_code(*self.url, status))
+        } else {
+            Ok(self)
+        }
+    }
+
+    /// Turn a reference to a response into an error if the server returned an error.
+    pub fn error_for_status_ref(&self) -> crate::Result<&Self> {
+        let status = self.status();
+        if status.is_client_error() || status.is_server_error() {
+            Err(crate::error::status_code(*self.url.clone(), status))
+        } else {
+            Ok(self)
+        }
+    }
+}
+
+impl fmt::Debug for Response {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        f.debug_struct("Response")
+            //.field("url", self.url())
+            .field("status", &self.status())
+            .field("headers", self.headers())
+            .finish()
+    }
+}
diff --git a/vendored-deps/wasm-timer/Cargo.toml b/vendored-deps/wasm-timer/Cargo.toml
new file mode 100644
index 000000000..ab2f6e2ac
--- /dev/null
+++ b/vendored-deps/wasm-timer/Cargo.toml
@@ -0,0 +1,22 @@
+[package]
+name = "wasm-timer"
+edition = "2018"
+description = "Abstraction over std::time::Instant and futures-timer that works on WASM"
+version = "0.2.5"
+authors = ["Pierre Krieger <pierre.krieger1708@gmail.com>"]
+license = "MIT"
+repository = "https://github.com/tomaka/wasm-timer"
+
+[dependencies]
+futures = "0.3"
+parking_lot = "0.12"
+pin-utils = "0.1"
+
+[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies]
+js-sys = "0.3"
+wasm-bindgen = "0.2"
+wasm-bindgen-futures = "0.4"
+web-sys = { version = "0.3", features = ["Performance", "Window"] }
+
+[dev-dependencies]
+async-std = "1.0"
diff --git a/vendored-deps/wasm-timer/LICENSE b/vendored-deps/wasm-timer/LICENSE
new file mode 100644
index 000000000..8acf927bb
--- /dev/null
+++ b/vendored-deps/wasm-timer/LICENSE
@@ -0,0 +1,19 @@
+Copyright 2019 Pierre Krieger
+Copyright (c) 2019 Tokio Contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
+OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/vendored-deps/wasm-timer/README.md b/vendored-deps/wasm-timer/README.md
new file mode 100644
index 000000000..70cd14f85
--- /dev/null
+++ b/vendored-deps/wasm-timer/README.md
@@ -0,0 +1,7 @@
+# Wasm-timer
+
+**This is a simple fork of the original repo at https://github.com/tomaka/wasm-timer**. The crate has
+not been updated since August 2020, and has a dependency on an older version of the `parking_lot`
+which breaks our WASM build (details here: https://github.com/Amanieu/parking_lot/issues/269).
+
+All we've done is update crate dependencies, in particualar bumping `parking_lot` to version `0.12`.
diff --git a/vendored-deps/wasm-timer/src/lib.rs b/vendored-deps/wasm-timer/src/lib.rs
new file mode 100644
index 000000000..391d0f0da
--- /dev/null
+++ b/vendored-deps/wasm-timer/src/lib.rs
@@ -0,0 +1,30 @@
+// Copyright 2019 Pierre Krieger
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the "Software"),
+// to deal in the Software without restriction, including without limitation
+// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+// and/or sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+// DEALINGS IN THE SOFTWARE.
+#![allow(clippy::all)]
+pub use timer::*;
+
+#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
+pub use std::time::{Instant, SystemTime, UNIX_EPOCH};
+#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
+pub use wasm::*;
+
+mod timer;
+#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
+mod wasm;
diff --git a/vendored-deps/wasm-timer/src/timer.rs b/vendored-deps/wasm-timer/src/timer.rs
new file mode 100644
index 000000000..32a64df5c
--- /dev/null
+++ b/vendored-deps/wasm-timer/src/timer.rs
@@ -0,0 +1,626 @@
+// The `timer` module is a copy-paste from the code of `futures-timer`, but
+// adjusted for WASM.
+//
+// Copyright (c) 2014 Alex Crichton
+//
+// Permission is hereby granted, free of charge, to any
+// person obtaining a copy of this software and associated
+// documentation files (the "Software"), to deal in the
+// Software without restriction, including without
+// limitation the rights to use, copy, modify, merge,
+// publish, distribute, sublicense, and/or sell copies of
+// the Software, and to permit persons to whom the Software
+// is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice
+// shall be included in all copies or substantial portions
+// of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
+// ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
+// TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
+// SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
+// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+// DEALINGS IN THE SOFTWARE.
+//
+// Apache License
+// Version 2.0, January 2004
+// http://www.apache.org/licenses/
+//
+// TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+//
+// 1. Definitions.
+//
+// "License" shall mean the terms and conditions for use, reproduction,
+// and distribution as defined by Sections 1 through 9 of this document.
+//
+// "Licensor" shall mean the copyright owner or entity authorized by
+// the copyright owner that is granting the License.
+//
+// "Legal Entity" shall mean the union of the acting entity and all
+// other entities that control, are controlled by, or are under common
+// control with that entity. For the purposes of this definition,
+// "control" means (i) the power, direct or indirect, to cause the
+// direction or management of such entity, whether by contract or
+// otherwise, or (ii) ownership of fifty percent (50%) or more of the
+// outstanding shares, or (iii) beneficial ownership of such entity.
+//
+// "You" (or "Your") shall mean an individual or Legal Entity
+// exercising permissions granted by this License.
+//
+// "Source" form shall mean the preferred form for making modifications,
+// including but not limited to software source code, documentation
+// source, and configuration files.
+//
+// "Object" form shall mean any form resulting from mechanical
+// transformation or translation of a Source form, including but
+// not limited to compiled object code, generated documentation,
+// and conversions to other media types.
+//
+// "Work" shall mean the work of authorship, whether in Source or
+// Object form, made available under the License, as indicated by a
+// copyright notice that is included in or attached to the work
+// (an example is provided in the Appendix below).
+//
+// "Derivative Works" shall mean any work, whether in Source or Object
+// form, that is based on (or derived from) the Work and for which the
+// editorial revisions, annotations, elaborations, or other modifications
+// represent, as a whole, an original work of authorship. For the purposes
+// of this License, Derivative Works shall not include works that remain
+// separable from, or merely link (or bind by name) to the interfaces of,
+// the Work and Derivative Works thereof.
+//
+// "Contribution" shall mean any work of authorship, including
+// the original version of the Work and any modifications or additions
+// to that Work or Derivative Works thereof, that is intentionally
+// submitted to Licensor for inclusion in the Work by the copyright owner
+// or by an individual or Legal Entity authorized to submit on behalf of
+// the copyright owner. For the purposes of this definition, "submitted"
+// means any form of electronic, verbal, or written communication sent
+// to the Licensor or its representatives, including but not limited to
+// communication on electronic mailing lists, source code control systems,
+// and issue tracking systems that are managed by, or on behalf of, the
+// Licensor for the purpose of discussing and improving the Work, but
+// excluding communication that is conspicuously marked or otherwise
+// designated in writing by the copyright owner as "Not a Contribution."
+//
+// "Contributor" shall mean Licensor and any individual or Legal Entity
+// on behalf of whom a Contribution has been received by Licensor and
+// subsequently incorporated within the Work.
+//
+// 2. Grant of Copyright License. Subject to the terms and conditions of
+// this License, each Contributor hereby grants to You a perpetual,
+// worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+// copyright license to reproduce, prepare Derivative Works of,
+// publicly display, publicly perform, sublicense, and distribute the
+// Work and such Derivative Works in Source or Object form.
+//
+// 3. Grant of Patent License. Subject to the terms and conditions of
+// this License, each Contributor hereby grants to You a perpetual,
+// worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+// (except as stated in this section) patent license to make, have made,
+// use, offer to sell, sell, import, and otherwise transfer the Work,
+// where such license applies only to those patent claims licensable
+// by such Contributor that are necessarily infringed by their
+// Contribution(s) alone or by combination of their Contribution(s)
+// with the Work to which such Contribution(s) was submitted. If You
+// institute patent litigation against any entity (including a
+// cross-claim or counterclaim in a lawsuit) alleging that the Work
+// or a Contribution incorporated within the Work constitutes direct
+// or contributory patent infringement, then any patent licenses
+// granted to You under this License for that Work shall terminate
+// as of the date such litigation is filed.
+//
+// 4. Redistribution. You may reproduce and distribute copies of the
+// Work or Derivative Works thereof in any medium, with or without
+// modifications, and in Source or Object form, provided that You
+// meet the following conditions:
+//
+// (a) You must give any other recipients of the Work or
+// Derivative Works a copy of this License; and
+//
+// (b) You must cause any modified files to carry prominent notices
+// stating that You changed the files; and
+//
+// (c) You must retain, in the Source form of any Derivative Works
+// that You distribute, all copyright, patent, trademark, and
+// attribution notices from the Source form of the Work,
+// excluding those notices that do not pertain to any part of
+// the Derivative Works; and
+//
+// (d) If the Work includes a "NOTICE" text file as part of its
+// distribution, then any Derivative Works that You distribute must
+// include a readable copy of the attribution notices contained
+// within such NOTICE file, excluding those notices that do not
+// pertain to any part of the Derivative Works, in at least one
+// of the following places: within a NOTICE text file distributed
+// as part of the Derivative Works; within the Source form or
+// documentation, if provided along with the Derivative Works; or,
+// within a display generated by the Derivative Works, if and
+// wherever such third-party notices normally appear. The contents
+// of the NOTICE file are for informational purposes only and
+// do not modify the License. You may add Your own attribution
+// notices within Derivative Works that You distribute, alongside
+// or as an addendum to the NOTICE text from the Work, provided
+// that such additional attribution notices cannot be construed
+// as modifying the License.
+//
+// You may add Your own copyright statement to Your modifications and
+// may provide additional or different license terms and conditions
+// for use, reproduction, or distribution of Your modifications, or
+// for any such Derivative Works as a whole, provided Your use,
+// reproduction, and distribution of the Work otherwise complies with
+// the conditions stated in this License.
+//
+// 5. Submission of Contributions. Unless You explicitly state otherwise,
+// any Contribution intentionally submitted for inclusion in the Work
+// by You to the Licensor shall be under the terms and conditions of
+// this License, without any additional terms or conditions.
+// Notwithstanding the above, nothing herein shall supersede or modify
+// the terms of any separate license agreement you may have executed
+// with Licensor regarding such Contributions.
+//
+// 6. Trademarks. This License does not grant permission to use the trade
+// names, trademarks, service marks, or product names of the Licensor,
+// except as required for reasonable and customary use in describing the
+// origin of the Work and reproducing the content of the NOTICE file.
+//
+// 7. Disclaimer of Warranty. Unless required by applicable law or
+// agreed to in writing, Licensor provides the Work (and each
+// Contributor provides its Contributions) on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+// implied, including, without limitation, any warranties or conditions
+// of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+// PARTICULAR PURPOSE. You are solely responsible for determining the
+// appropriateness of using or redistributing the Work and assume any
+// risks associated with Your exercise of permissions under this License.
+//
+// 8. Limitation of Liability. In no event and under no legal theory,
+// whether in tort (including negligence), contract, or otherwise,
+// unless required by applicable law (such as deliberate and grossly
+// negligent acts) or agreed to in writing, shall any Contributor be
+// liable to You for damages, including any direct, indirect, special,
+// incidental, or consequential damages of any character arising as a
+// result of this License or out of the use or inability to use the
+// Work (including but not limited to damages for loss of goodwill,
+// work stoppage, computer failure or malfunction, or any and all
+// other commercial damages or losses), even if such Contributor
+// has been advised of the possibility of such damages.
+//
+// 9. Accepting Warranty or Additional Liability. While redistributing
+// the Work or Derivative Works thereof, You may choose to offer,
+// and charge a fee for, acceptance of support, warranty, indemnity,
+// or other liability obligations and/or rights consistent with this
+// License. However, in accepting such obligations, You may act only
+// on Your own behalf and on Your sole responsibility, not on behalf
+// of any other Contributor, and only if You agree to indemnify,
+// defend, and hold each Contributor harmless for any liability
+// incurred by, or claims asserted against, such Contributor by reason
+// of your accepting any such warranty or additional liability.
+//
+// END OF TERMS AND CONDITIONS
+//
+// APPENDIX: How to apply the Apache License to your work.
+//
+// To apply the Apache License to your work, attach the following
+// boilerplate notice, with the fields enclosed by brackets "[]"
+// replaced with your own identifying information. (Don't include
+// the brackets!)  The text should be enclosed in the appropriate
+// comment syntax for the file format. We also recommend that a
+// file or class name and description of purpose be included on the
+// same "printed page" as the copyright notice for easier
+// identification within third-party archives.
+//
+// Copyright [yyyy] [name of copyright owner]
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+use crate::Instant;
+use std::cmp::Ordering;
+use std::fmt;
+use std::mem;
+use std::pin::Pin;
+use std::sync::atomic::AtomicUsize;
+use std::sync::atomic::Ordering::SeqCst;
+use std::sync::{Arc, Mutex, Weak};
+use std::task::{Context, Poll};
+
+use futures::prelude::*;
+use futures::task::AtomicWaker;
+
+use arc_list::{ArcList, Node};
+use heap::{Heap, Slot};
+
+mod arc_list;
+mod global;
+mod heap;
+
+pub mod ext;
+pub use ext::{TryFutureExt, TryStreamExt};
+
+/// A "timer heap" used to power separately owned instances of `Delay` and
+/// `Interval`.
+///
+/// This timer is implemented as a priority queued-based heap. Each `Timer`
+/// contains a few primary methods which which to drive it:
+///
+/// * `next_wake` indicates how long the ambient system needs to sleep until it
+///   invokes further processing on a `Timer`
+/// * `advance_to` is what actually fires timers on the `Timer`, and should be
+///   called essentially every iteration of the event loop, or when the time
+///   specified by `next_wake` has elapsed.
+/// * The `Future` implementation for `Timer` is used to process incoming timer
+///   updates and requests. This is used to schedule new timeouts, update
+///   existing ones, or delete existing timeouts. The `Future` implementation
+///   will never resolve, but it'll schedule notifications of when to wake up
+///   and process more messages.
+///
+/// Note that if you're using this crate you probably don't need to use a
+/// `Timer` as there is a global one already available for you run on a helper
+/// thread. If this isn't desirable, though, then the
+/// `TimerHandle::set_fallback` method can be used instead!
+pub struct Timer {
+    inner: Arc<Inner>,
+    timer_heap: Heap<HeapTimer>,
+}
+
+/// A handle to a `Timer` which is used to create instances of a `Delay`.
+#[derive(Clone)]
+pub struct TimerHandle {
+    inner: Weak<Inner>,
+}
+
+mod delay;
+mod interval;
+pub use self::delay::Delay;
+pub use self::interval::Interval;
+
+struct Inner {
+    /// List of updates the `Timer` needs to process
+    list: ArcList<ScheduledTimer>,
+
+    /// The blocked `Timer` task to receive notifications to the `list` above.
+    waker: AtomicWaker,
+}
+
+/// Shared state between the `Timer` and a `Delay`.
+struct ScheduledTimer {
+    waker: AtomicWaker,
+
+    // The lowest bit here is whether the timer has fired or not, the second
+    // lowest bit is whether the timer has been invalidated, and all the other
+    // bits are the "generation" of the timer which is reset during the `reset`
+    // function. Only timers for a matching generation are fired.
+    state: AtomicUsize,
+
+    inner: Weak<Inner>,
+    at: Mutex<Option<Instant>>,
+
+    // TODO: this is only accessed by the timer thread, should have a more
+    // lightweight protection than a `Mutex`
+    slot: Mutex<Option<Slot>>,
+}
+
+/// Entries in the timer heap, sorted by the instant they're firing at and then
+/// also containing some payload data.
+struct HeapTimer {
+    at: Instant,
+    gen: usize,
+    node: Arc<Node<ScheduledTimer>>,
+}
+
+impl Timer {
+    /// Creates a new timer heap ready to create new timers.
+    pub fn new() -> Timer {
+        Timer {
+            inner: Arc::new(Inner {
+                list: ArcList::new(),
+                waker: AtomicWaker::new(),
+            }),
+            timer_heap: Heap::new(),
+        }
+    }
+
+    /// Returns a handle to this timer heap, used to create new timeouts.
+    pub fn handle(&self) -> TimerHandle {
+        TimerHandle {
+            inner: Arc::downgrade(&self.inner),
+        }
+    }
+
+    /// Returns the time at which this timer next needs to be invoked with
+    /// `advance_to`.
+    ///
+    /// Event loops or threads typically want to sleep until the specified
+    /// instant.
+    pub fn next_event(&self) -> Option<Instant> {
+        self.timer_heap.peek().map(|t| t.at)
+    }
+
+    /// Proces any timers which are supposed to fire at or before the current
+    /// instant.
+    ///
+    /// This method is equivalent to `self.advance_to(Instant::now())`.
+    pub fn advance(&mut self) {
+        self.advance_to(Instant::now())
+    }
+
+    /// Proces any timers which are supposed to fire before `now` specified.
+    ///
+    /// This method should be called on `Timer` periodically to advance the
+    /// internal state and process any pending timers which need to fire.
+    pub fn advance_to(&mut self, now: Instant) {
+        loop {
+            match self.timer_heap.peek() {
+                Some(head) if head.at <= now => {}
+                Some(_) => break,
+                None => break,
+            };
+
+            // Flag the timer as fired and then notify its task, if any, that's
+            // blocked.
+            let heap_timer = self.timer_heap.pop().unwrap();
+            *heap_timer.node.slot.lock().unwrap() = None;
+            let bits = heap_timer.gen << 2;
+            match heap_timer
+                .node
+                .state
+                .compare_exchange(bits, bits | 0b01, SeqCst, SeqCst)
+            {
+                Ok(_) => heap_timer.node.waker.wake(),
+                Err(_b) => {}
+            }
+        }
+    }
+
+    /// Either updates the timer at slot `idx` to fire at `at`, or adds a new
+    /// timer at `idx` and sets it to fire at `at`.
+    fn update_or_add(&mut self, at: Instant, node: Arc<Node<ScheduledTimer>>) {
+        // TODO: avoid remove + push and instead just do one sift of the heap?
+        // In theory we could update it in place and then do the percolation
+        // as necessary
+        let gen = node.state.load(SeqCst) >> 2;
+        let mut slot = node.slot.lock().unwrap();
+        if let Some(heap_slot) = slot.take() {
+            self.timer_heap.remove(heap_slot);
+        }
+        *slot = Some(self.timer_heap.push(HeapTimer {
+            at: at,
+            gen: gen,
+            node: node.clone(),
+        }));
+    }
+
+    fn remove(&mut self, node: Arc<Node<ScheduledTimer>>) {
+        // If this `idx` is still around and it's still got a registered timer,
+        // then we jettison it form the timer heap.
+        let mut slot = node.slot.lock().unwrap();
+        let heap_slot = match slot.take() {
+            Some(slot) => slot,
+            None => return,
+        };
+        self.timer_heap.remove(heap_slot);
+    }
+
+    fn invalidate(&mut self, node: Arc<Node<ScheduledTimer>>) {
+        node.state.fetch_or(0b10, SeqCst);
+        node.waker.wake();
+    }
+}
+
+impl Future for Timer {
+    type Output = ();
+
+    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
+        Pin::new(&mut self.inner).waker.register(cx.waker());
+        let mut list = self.inner.list.take();
+        while let Some(node) = list.pop() {
+            let at = *node.at.lock().unwrap();
+            match at {
+                Some(at) => self.update_or_add(at, node),
+                None => self.remove(node),
+            }
+        }
+        Poll::Pending
+    }
+}
+
+impl Drop for Timer {
+    fn drop(&mut self) {
+        // Seal off our list to prevent any more updates from getting pushed on.
+        // Any timer which sees an error from the push will immediately become
+        // inert.
+        let mut list = self.inner.list.take_and_seal();
+
+        // Now that we'll never receive another timer, drain the list of all
+        // updates and also drain our heap of all active timers, invalidating
+        // everything.
+        while let Some(t) = list.pop() {
+            self.invalidate(t);
+        }
+        while let Some(t) = self.timer_heap.pop() {
+            self.invalidate(t.node);
+        }
+    }
+}
+
+impl fmt::Debug for Timer {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
+        f.debug_struct("Timer").field("heap", &"...").finish()
+    }
+}
+
+impl PartialEq for HeapTimer {
+    fn eq(&self, other: &HeapTimer) -> bool {
+        self.at == other.at
+    }
+}
+
+impl Eq for HeapTimer {}
+
+impl PartialOrd for HeapTimer {
+    fn partial_cmp(&self, other: &HeapTimer) -> Option<Ordering> {
+        Some(self.cmp(other))
+    }
+}
+
+impl Ord for HeapTimer {
+    fn cmp(&self, other: &HeapTimer) -> Ordering {
+        self.at.cmp(&other.at)
+    }
+}
+
+static HANDLE_FALLBACK: AtomicUsize = AtomicUsize::new(0);
+
+/// Error returned from `TimerHandle::set_fallback`.
+#[derive(Clone, Debug)]
+pub struct SetDefaultError(());
+
+impl TimerHandle {
+    /// Configures this timer handle to be the one returned by
+    /// `TimerHandle::default`.
+    ///
+    /// By default a global thread is initialized on the first call to
+    /// `TimerHandle::default`. This first call can happen transitively through
+    /// `Delay::new`. If, however, that hasn't happened yet then the global
+    /// default timer handle can be configured through this method.
+    ///
+    /// This method can be used to prevent the global helper thread from
+    /// spawning. If this method is successful then the global helper thread
+    /// will never get spun up.
+    ///
+    /// On success this timer handle will have installed itself globally to be
+    /// used as the return value for `TimerHandle::default` unless otherwise
+    /// specified.
+    ///
+    /// # Errors
+    ///
+    /// If another thread has already called `set_as_global_fallback` or this
+    /// thread otherwise loses a race to call this method then it will fail
+    /// returning an error. Once a call to `set_as_global_fallback` is
+    /// successful then no future calls may succeed.
+    pub fn set_as_global_fallback(self) -> Result<(), SetDefaultError> {
+        unsafe {
+            let val = self.into_usize();
+            match HANDLE_FALLBACK.compare_exchange(0, val, SeqCst, SeqCst) {
+                Ok(_) => Ok(()),
+                Err(_) => {
+                    drop(TimerHandle::from_usize(val));
+                    Err(SetDefaultError(()))
+                }
+            }
+        }
+    }
+
+    fn into_usize(self) -> usize {
+        unsafe { mem::transmute::<Weak<Inner>, usize>(self.inner) }
+    }
+
+    unsafe fn from_usize(val: usize) -> TimerHandle {
+        let inner = mem::transmute::<usize, Weak<Inner>>(val);
+        TimerHandle { inner }
+    }
+}
+
+impl Default for TimerHandle {
+    #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
+    fn default() -> TimerHandle {
+        let mut fallback = HANDLE_FALLBACK.load(SeqCst);
+
+        // If the fallback hasn't been previously initialized then let's spin
+        // up a helper thread and try to initialize with that. If we can't
+        // actually create a helper thread then we'll just return a "defunkt"
+        // handle which will return errors when timer objects are attempted to
+        // be associated.
+        if fallback == 0 {
+            let helper = match global::HelperThread::new() {
+                Ok(helper) => helper,
+                Err(_) => return TimerHandle { inner: Weak::new() },
+            };
+
+            // If we successfully set ourselves as the actual fallback then we
+            // want to `forget` the helper thread to ensure that it persists
+            // globally. If we fail to set ourselves as the fallback that means
+            // that someone was racing with this call to
+            // `TimerHandle::default`.  They ended up winning so we'll destroy
+            // our helper thread (which shuts down the thread) and reload the
+            // fallback.
+            if helper.handle().set_as_global_fallback().is_ok() {
+                let ret = helper.handle();
+                helper.forget();
+                return ret;
+            }
+            fallback = HANDLE_FALLBACK.load(SeqCst);
+        }
+
+        // At this point our fallback handle global was configured so we use
+        // its value to reify a handle, clone it, and then forget our reified
+        // handle as we don't actually have an owning reference to it.
+        assert!(fallback != 0);
+        unsafe {
+            let handle = TimerHandle::from_usize(fallback);
+            let ret = handle.clone();
+            drop(handle.into_usize());
+            return ret;
+        }
+    }
+
+    #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
+    fn default() -> TimerHandle {
+        let mut fallback = HANDLE_FALLBACK.load(SeqCst);
+
+        // If the fallback hasn't been previously initialized then let's spin
+        // up a helper thread and try to initialize with that. If we can't
+        // actually create a helper thread then we'll just return a "defunkt"
+        // handle which will return errors when timer objects are attempted to
+        // be associated.
+        if fallback == 0 {
+            let handle = global::run();
+
+            // If we successfully set ourselves as the actual fallback then we
+            // want to `forget` the helper thread to ensure that it persists
+            // globally. If we fail to set ourselves as the fallback that means
+            // that someone was racing with this call to
+            // `TimerHandle::default`.  They ended up winning so we'll destroy
+            // our helper thread (which shuts down the thread) and reload the
+            // fallback.
+            if handle.clone().set_as_global_fallback().is_ok() {
+                return handle;
+            }
+            fallback = HANDLE_FALLBACK.load(SeqCst);
+        }
+
+        // At this point our fallback handle global was configured so we use
+        // its value to reify a handle, clone it, and then forget our reified
+        // handle as we don't actually have an owning reference to it.
+        assert!(fallback != 0);
+        unsafe {
+            let handle = TimerHandle::from_usize(fallback);
+            let ret = handle.clone();
+            drop(handle.into_usize());
+            return ret;
+        }
+    }
+}
+
+impl fmt::Debug for TimerHandle {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
+        f.debug_struct("TimerHandle")
+            .field("inner", &"...")
+            .finish()
+    }
+}
diff --git a/vendored-deps/wasm-timer/src/timer/arc_list.rs b/vendored-deps/wasm-timer/src/timer/arc_list.rs
new file mode 100644
index 000000000..43880523d
--- /dev/null
+++ b/vendored-deps/wasm-timer/src/timer/arc_list.rs
@@ -0,0 +1,155 @@
+//! An atomically managed intrusive linked list of `Arc` nodes
+
+use std::marker;
+use std::ops::Deref;
+use std::sync::atomic::Ordering::SeqCst;
+use std::sync::atomic::{AtomicBool, AtomicUsize};
+use std::sync::Arc;
+
+pub struct ArcList<T> {
+    list: AtomicUsize,
+    _marker: marker::PhantomData<T>,
+}
+
+impl<T> ArcList<T> {
+    pub fn new() -> ArcList<T> {
+        ArcList {
+            list: AtomicUsize::new(0),
+            _marker: marker::PhantomData,
+        }
+    }
+
+    /// Pushes the `data` provided onto this list if it's not already enqueued
+    /// in this list.
+    ///
+    /// If `data` is already enqueued in this list then this is a noop,
+    /// otherwise, the `data` here is pushed on the end of the list.
+    pub fn push(&self, data: &Arc<Node<T>>) -> Result<(), ()> {
+        if data.enqueued.swap(true, SeqCst) {
+            // note that even if our list is sealed off then the other end is
+            // still guaranteed to see us because we were previously enqueued.
+            return Ok(());
+        }
+        let mut head = self.list.load(SeqCst);
+        let node = Arc::into_raw(data.clone()) as usize;
+        loop {
+            // If we've been sealed off, abort and return an error
+            if head == 1 {
+                unsafe {
+                    drop(Arc::from_raw(node as *mut Node<T>));
+                }
+                return Err(());
+            }
+
+            // Otherwise attempt to push this node
+            data.next.store(head, SeqCst);
+            match self.list.compare_exchange(head, node, SeqCst, SeqCst) {
+                Ok(_) => break Ok(()),
+                Err(new_head) => head = new_head,
+            }
+        }
+    }
+
+    /// Atomically empties this list, returning a new owned copy which can be
+    /// used to iterate over the entries.
+    pub fn take(&self) -> ArcList<T> {
+        let mut list = self.list.load(SeqCst);
+        loop {
+            if list == 1 {
+                break;
+            }
+            match self.list.compare_exchange(list, 0, SeqCst, SeqCst) {
+                Ok(_) => break,
+                Err(l) => list = l,
+            }
+        }
+        ArcList {
+            list: AtomicUsize::new(list),
+            _marker: marker::PhantomData,
+        }
+    }
+
+    /// Atomically empties this list and prevents further successful calls to
+    /// `push`.
+    pub fn take_and_seal(&self) -> ArcList<T> {
+        ArcList {
+            list: AtomicUsize::new(self.list.swap(1, SeqCst)),
+            _marker: marker::PhantomData,
+        }
+    }
+
+    /// Removes the head of the list of nodes, returning `None` if this is an
+    /// empty list.
+    pub fn pop(&mut self) -> Option<Arc<Node<T>>> {
+        let head = *self.list.get_mut();
+        if head == 0 || head == 1 {
+            return None;
+        }
+        let head = unsafe { Arc::from_raw(head as *const Node<T>) };
+        *self.list.get_mut() = head.next.load(SeqCst);
+        // At this point, the node is out of the list, so store `false` so we
+        // can enqueue it again and see further changes.
+        assert!(head.enqueued.swap(false, SeqCst));
+        Some(head)
+    }
+}
+
+impl<T> Drop for ArcList<T> {
+    fn drop(&mut self) {
+        while let Some(_) = self.pop() {
+            // ...
+        }
+    }
+}
+
+pub struct Node<T> {
+    next: AtomicUsize,
+    enqueued: AtomicBool,
+    data: T,
+}
+
+impl<T> Node<T> {
+    pub fn new(data: T) -> Node<T> {
+        Node {
+            next: AtomicUsize::new(0),
+            enqueued: AtomicBool::new(false),
+            data: data,
+        }
+    }
+}
+
+impl<T> Deref for Node<T> {
+    type Target = T;
+
+    fn deref(&self) -> &T {
+        &self.data
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn smoke() {
+        let a = ArcList::new();
+        let n = Arc::new(Node::new(1));
+        assert!(a.push(&n).is_ok());
+
+        let mut l = a.take();
+        assert_eq!(**l.pop().unwrap(), 1);
+        assert!(l.pop().is_none());
+    }
+
+    #[test]
+    fn seal() {
+        let a = ArcList::new();
+        let n = Arc::new(Node::new(1));
+        let mut l = a.take_and_seal();
+        assert!(l.pop().is_none());
+        assert!(a.push(&n).is_err());
+
+        assert!(a.take().pop().is_none());
+        assert!(a.take_and_seal().pop().is_none());
+    }
+}
diff --git a/vendored-deps/wasm-timer/src/timer/delay.rs b/vendored-deps/wasm-timer/src/timer/delay.rs
new file mode 100644
index 000000000..23f630745
--- /dev/null
+++ b/vendored-deps/wasm-timer/src/timer/delay.rs
@@ -0,0 +1,208 @@
+//! Support for creating futures that represent timeouts.
+//!
+//! This module contains the `Delay` type which is a future that will resolve
+//! at a particular point in the future.
+
+use std::fmt;
+use std::future::Future;
+use std::io;
+use std::pin::Pin;
+use std::sync::atomic::AtomicUsize;
+use std::sync::atomic::Ordering::SeqCst;
+use std::sync::{Arc, Mutex};
+use std::task::{Context, Poll};
+use std::time::Duration;
+
+use futures::task::AtomicWaker;
+
+use crate::timer::arc_list::Node;
+use crate::timer::{ScheduledTimer, TimerHandle};
+use crate::Instant;
+
+/// A future representing the notification that an elapsed duration has
+/// occurred.
+///
+/// This is created through the `Delay::new` or `Delay::new_at` methods
+/// indicating when the future should fire at.  Note that these futures are not
+/// intended for high resolution timers, but rather they will likely fire some
+/// granularity after the exact instant that they're otherwise indicated to
+/// fire at.
+pub struct Delay {
+    state: Option<Arc<Node<ScheduledTimer>>>,
+    when: Instant,
+}
+
+impl Delay {
+    /// Creates a new future which will fire at `dur` time into the future.
+    ///
+    /// The returned object will be bound to the default timer for this thread.
+    /// The default timer will be spun up in a helper thread on first use.
+    #[inline]
+    pub fn new(dur: Duration) -> Delay {
+        Delay::new_at(Instant::now() + dur)
+    }
+
+    /// Creates a new future which will fire at the time specified by `at`.
+    ///
+    /// The returned object will be bound to the default timer for this thread.
+    /// The default timer will be spun up in a helper thread on first use.
+    #[inline]
+    pub fn new_at(at: Instant) -> Delay {
+        Delay::new_handle(at, Default::default())
+    }
+
+    /// Creates a new future which will fire at the time specified by `at`.
+    ///
+    /// The returned instance of `Delay` will be bound to the timer specified by
+    /// the `handle` argument.
+    pub fn new_handle(at: Instant, handle: TimerHandle) -> Delay {
+        let inner = match handle.inner.upgrade() {
+            Some(i) => i,
+            None => {
+                return Delay {
+                    state: None,
+                    when: at,
+                }
+            }
+        };
+        let state = Arc::new(Node::new(ScheduledTimer {
+            at: Mutex::new(Some(at)),
+            state: AtomicUsize::new(0),
+            waker: AtomicWaker::new(),
+            inner: handle.inner,
+            slot: Mutex::new(None),
+        }));
+
+        // If we fail to actually push our node then we've become an inert
+        // timer, meaning that we'll want to immediately return an error from
+        // `poll`.
+        if inner.list.push(&state).is_err() {
+            return Delay {
+                state: None,
+                when: at,
+            };
+        }
+
+        inner.waker.wake();
+        Delay {
+            state: Some(state),
+            when: at,
+        }
+    }
+
+    /// Resets this timeout to an new timeout which will fire at the time
+    /// specified by `dur`.
+    ///
+    /// This is equivalent to calling `reset_at` with `Instant::now() + dur`
+    #[inline]
+    pub fn reset(&mut self, dur: Duration) {
+        self.reset_at(Instant::now() + dur)
+    }
+
+    /// Resets this timeout to an new timeout which will fire at the time
+    /// specified by `at`.
+    ///
+    /// This method is usable even of this instance of `Delay` has "already
+    /// fired". That is, if this future has resovled, calling this method means
+    /// that the future will still re-resolve at the specified instant.
+    ///
+    /// If `at` is in the past then this future will immediately be resolved
+    /// (when `poll` is called).
+    ///
+    /// Note that if any task is currently blocked on this future then that task
+    /// will be dropped. It is required to call `poll` again after this method
+    /// has been called to ensure tha ta task is blocked on this future.
+    #[inline]
+    pub fn reset_at(&mut self, at: Instant) {
+        self.when = at;
+        if self._reset(at).is_err() {
+            self.state = None
+        }
+    }
+
+    fn _reset(&mut self, at: Instant) -> Result<(), ()> {
+        let state = match self.state {
+            Some(ref state) => state,
+            None => return Err(()),
+        };
+        if let Some(timeouts) = state.inner.upgrade() {
+            let mut bits = state.state.load(SeqCst);
+            loop {
+                // If we've been invalidated, cancel this reset
+                if bits & 0b10 != 0 {
+                    return Err(());
+                }
+                let new = bits.wrapping_add(0b100) & !0b11;
+                match state.state.compare_exchange(bits, new, SeqCst, SeqCst) {
+                    Ok(_) => break,
+                    Err(s) => bits = s,
+                }
+            }
+            *state.at.lock().unwrap() = Some(at);
+            // If we fail to push our node then we've become an inert timer, so
+            // we'll want to clear our `state` field accordingly
+            timeouts.list.push(state)?;
+            timeouts.waker.wake();
+        }
+
+        Ok(())
+    }
+}
+
+#[inline]
+pub fn fires_at(timeout: &Delay) -> Instant {
+    timeout.when
+}
+
+impl Future for Delay {
+    type Output = io::Result<()>;
+
+    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
+        let state = match self.state {
+            Some(ref state) => state,
+            None => {
+                let err = Err(io::Error::new(io::ErrorKind::Other, "timer has gone away"));
+                return Poll::Ready(err);
+            }
+        };
+
+        if state.state.load(SeqCst) & 1 != 0 {
+            return Poll::Ready(Ok(()));
+        }
+
+        state.waker.register(&cx.waker());
+
+        // Now that we've registered, do the full check of our own internal
+        // state. If we've fired the first bit is set, and if we've been
+        // invalidated the second bit is set.
+        match state.state.load(SeqCst) {
+            n if n & 0b01 != 0 => Poll::Ready(Ok(())),
+            n if n & 0b10 != 0 => Poll::Ready(Err(io::Error::new(
+                io::ErrorKind::Other,
+                "timer has gone away",
+            ))),
+            _ => Poll::Pending,
+        }
+    }
+}
+
+impl Drop for Delay {
+    fn drop(&mut self) {
+        let state = match self.state {
+            Some(ref s) => s,
+            None => return,
+        };
+        if let Some(timeouts) = state.inner.upgrade() {
+            *state.at.lock().unwrap() = None;
+            if timeouts.list.push(state).is_ok() {
+                timeouts.waker.wake();
+            }
+        }
+    }
+}
+
+impl fmt::Debug for Delay {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
+        f.debug_struct("Delay").field("when", &self.when).finish()
+    }
+}
diff --git a/vendored-deps/wasm-timer/src/timer/ext.rs b/vendored-deps/wasm-timer/src/timer/ext.rs
new file mode 100644
index 000000000..41124c846
--- /dev/null
+++ b/vendored-deps/wasm-timer/src/timer/ext.rs
@@ -0,0 +1,200 @@
+//! Extension traits for the standard `Stream` and `Future` traits.
+
+use std::io;
+use std::pin::Pin;
+use std::task::{Context, Poll};
+use std::time::Duration;
+
+use futures::prelude::*;
+use pin_utils::unsafe_pinned;
+
+use crate::{Delay, Instant};
+
+/// An extension trait for futures which provides convenient accessors for
+/// timing out execution and such.
+pub trait TryFutureExt: TryFuture + Sized {
+    /// Creates a new future which will take at most `dur` time to resolve from
+    /// the point at which this method is called.
+    ///
+    /// This combinator creates a new future which wraps the receiving future
+    /// in a timeout. The future returned will resolve in at most `dur` time
+    /// specified (relative to when this function is called).
+    ///
+    /// If the future completes before `dur` elapses then the future will
+    /// resolve with that item. Otherwise the future will resolve to an error
+    /// once `dur` has elapsed.
+    ///
+    /// # Examples
+    ///
+    /// ```no_run
+    /// use std::time::Duration;
+    /// use futures::prelude::*;
+    /// use wasm_timer::TryFutureExt;
+    ///
+    /// # fn long_future() -> impl TryFuture<Ok = (), Error = std::io::Error> {
+    /// #     futures::future::ok(())
+    /// # }
+    /// #
+    /// fn main() {
+    ///     let future = long_future();
+    ///     let timed_out = future.timeout(Duration::from_secs(1));
+    ///
+    ///     async_std::task::block_on(async {
+    ///         match timed_out.await {
+    ///             Ok(item) => println!("got {:?} within enough time!", item),
+    ///             Err(_) => println!("took too long to produce the item"),
+    ///         }
+    ///     })
+    /// }
+    /// ```
+    fn timeout(self, dur: Duration) -> Timeout<Self>
+    where
+        Self::Error: From<io::Error>,
+    {
+        Timeout {
+            timeout: Delay::new(dur),
+            future: self,
+        }
+    }
+
+    /// Creates a new future which will resolve no later than `at` specified.
+    ///
+    /// This method is otherwise equivalent to the `timeout` method except that
+    /// it tweaks the moment at when the timeout elapsed to being specified with
+    /// an absolute value rather than a relative one. For more documentation see
+    /// the `timeout` method.
+    fn timeout_at(self, at: Instant) -> Timeout<Self>
+    where
+        Self::Error: From<io::Error>,
+    {
+        Timeout {
+            timeout: Delay::new_at(at),
+            future: self,
+        }
+    }
+}
+
+impl<F: TryFuture> TryFutureExt for F {}
+
+/// Future returned by the `FutureExt::timeout` method.
+#[derive(Debug)]
+pub struct Timeout<F>
+where
+    F: TryFuture,
+    F::Error: From<io::Error>,
+{
+    future: F,
+    timeout: Delay,
+}
+
+impl<F> Timeout<F>
+where
+    F: TryFuture,
+    F::Error: From<io::Error>,
+{
+    unsafe_pinned!(future: F);
+    unsafe_pinned!(timeout: Delay);
+}
+
+impl<F> Future for Timeout<F>
+where
+    F: TryFuture,
+    F::Error: From<io::Error>,
+{
+    type Output = Result<F::Ok, F::Error>;
+
+    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
+        match self.as_mut().future().try_poll(cx) {
+            Poll::Pending => {}
+            other => return other,
+        }
+
+        if self.timeout().poll(cx).is_ready() {
+            let err = Err(io::Error::new(io::ErrorKind::TimedOut, "future timed out").into());
+            Poll::Ready(err)
+        } else {
+            Poll::Pending
+        }
+    }
+}
+
+/// An extension trait for streams which provides convenient accessors for
+/// timing out execution and such.
+pub trait TryStreamExt: TryStream + Sized {
+    /// Creates a new stream which will take at most `dur` time to yield each
+    /// item of the stream.
+    ///
+    /// This combinator creates a new stream which wraps the receiving stream
+    /// in a timeout-per-item. The stream returned will resolve in at most
+    /// `dur` time for each item yielded from the stream. The first item's timer
+    /// starts when this method is called.
+    ///
+    /// If a stream's item completes before `dur` elapses then the timer will be
+    /// reset for the next item. If the timeout elapses, however, then an error
+    /// will be yielded on the stream and the timer will be reset.
+    fn timeout(self, dur: Duration) -> TimeoutStream<Self>
+    where
+        Self::Error: From<io::Error>,
+    {
+        TimeoutStream {
+            timeout: Delay::new(dur),
+            dur,
+            stream: self,
+        }
+    }
+}
+
+impl<S: TryStream> TryStreamExt for S {}
+
+/// Stream returned by the `StreamExt::timeout` method.
+#[derive(Debug)]
+pub struct TimeoutStream<S>
+where
+    S: TryStream,
+    S::Error: From<io::Error>,
+{
+    timeout: Delay,
+    dur: Duration,
+    stream: S,
+}
+
+impl<S> TimeoutStream<S>
+where
+    S: TryStream,
+    S::Error: From<io::Error>,
+{
+    unsafe_pinned!(timeout: Delay);
+    unsafe_pinned!(stream: S);
+}
+
+impl<S> Stream for TimeoutStream<S>
+where
+    S: TryStream,
+    S::Error: From<io::Error>,
+{
+    type Item = Result<S::Ok, S::Error>;
+
+    fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
+        let dur = self.dur;
+
+        let r = self.as_mut().stream().try_poll_next(cx);
+        match r {
+            Poll::Pending => {}
+            other => {
+                self.as_mut().timeout().reset(dur);
+                return other;
+            }
+        }
+
+        if self.as_mut().timeout().poll(cx).is_ready() {
+            self.as_mut().timeout().reset(dur);
+            Poll::Ready(Some(Err(io::Error::new(
+                io::ErrorKind::TimedOut,
+                "stream item timed out",
+            )
+            .into())))
+        } else {
+            Poll::Pending
+        }
+    }
+}
diff --git a/vendored-deps/wasm-timer/src/timer/global.rs b/vendored-deps/wasm-timer/src/timer/global.rs
new file mode 100644
index 000000000..6f70c5c3d
--- /dev/null
+++ b/vendored-deps/wasm-timer/src/timer/global.rs
@@ -0,0 +1,8 @@
+pub(crate) use self::platform::*;
+
+#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
+#[path = "global/desktop.rs"]
+mod platform;
+#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
+#[path = "global/wasm.rs"]
+mod platform;
diff --git a/vendored-deps/wasm-timer/src/timer/global/desktop.rs b/vendored-deps/wasm-timer/src/timer/global/desktop.rs
new file mode 100644
index 000000000..072daac74
--- /dev/null
+++ b/vendored-deps/wasm-timer/src/timer/global/desktop.rs
@@ -0,0 +1,106 @@
+use std::future::Future;
+use std::io;
+use std::mem::{self, ManuallyDrop};
+use std::sync::atomic::{AtomicBool, Ordering};
+use std::sync::Arc;
+use std::task::{Context, RawWaker, RawWakerVTable, Waker};
+use std::thread;
+use std::thread::Thread;
+use std::time::Instant;
+
+use pin_utils::pin_mut;
+
+use crate::{Timer, TimerHandle};
+
+pub struct HelperThread {
+    thread: Option<thread::JoinHandle<()>>,
+    timer: TimerHandle,
+    done: Arc<AtomicBool>,
+}
+
+impl HelperThread {
+    pub fn new() -> io::Result<HelperThread> {
+        let timer = Timer::new();
+        let timer_handle = timer.handle();
+        let done = Arc::new(AtomicBool::new(false));
+        let done2 = done.clone();
+        let thread = thread::Builder::new().spawn(move || run(timer, done2))?;
+
+        Ok(HelperThread {
+            thread: Some(thread),
+            done,
+            timer: timer_handle,
+        })
+    }
+
+    pub fn handle(&self) -> TimerHandle {
+        self.timer.clone()
+    }
+
+    pub fn forget(mut self) {
+        self.thread.take();
+    }
+}
+
+impl Drop for HelperThread {
+    fn drop(&mut self) {
+        let thread = match self.thread.take() {
+            Some(thread) => thread,
+            None => return,
+        };
+        self.done.store(true, Ordering::SeqCst);
+        thread.thread().unpark();
+        drop(thread.join());
+    }
+}
+
+fn run(timer: Timer, done: Arc<AtomicBool>) {
+    let mut waker = current_thread_waker();
+    let mut cx = Context::from_waker(&mut waker);
+
+    pin_mut!(timer);
+    while !done.load(Ordering::SeqCst) {
+        drop(timer.as_mut().poll(&mut cx));
+
+        timer.advance();
+        match timer.next_event() {
+            // Ok, block for the specified time
+            Some(when) => {
+                let now = Instant::now();
+                if now < when {
+                    thread::park_timeout(when - now)
+                } else {
+                    // .. continue...
+                }
+            }
+
+            // Just wait for one of our futures to wake up
+            None => thread::park(),
+        }
+    }
+}
+
+static VTABLE: RawWakerVTable = RawWakerVTable::new(raw_clone, raw_wake, raw_wake_by_ref, raw_drop);
+
+fn raw_clone(ptr: *const ()) -> RawWaker {
+    let me = ManuallyDrop::new(unsafe { Arc::from_raw(ptr as *const Thread) });
+    mem::forget(me.clone());
+    RawWaker::new(ptr, &VTABLE)
+}
+
+fn raw_wake(ptr: *const ()) {
+    unsafe { Arc::from_raw(ptr as *const Thread) }.unpark()
+}
+
+fn raw_wake_by_ref(ptr: *const ()) {
+    ManuallyDrop::new(unsafe { Arc::from_raw(ptr as *const Thread) }).unpark()
+}
+
+fn raw_drop(ptr: *const ()) {
+    unsafe { Arc::from_raw(ptr as *const Thread) };
+}
+
+fn current_thread_waker() -> Waker {
+    let thread = Arc::new(thread::current());
+    unsafe { Waker::from_raw(raw_clone(Arc::into_raw(thread) as *const ())) }
+}
diff --git a/vendored-deps/wasm-timer/src/timer/global/wasm.rs b/vendored-deps/wasm-timer/src/timer/global/wasm.rs
new file mode 100644
index 000000000..7d75b31f1
--- /dev/null
+++ b/vendored-deps/wasm-timer/src/timer/global/wasm.rs
@@ -0,0 +1,82 @@
+use futures::task::{self, ArcWake};
+use parking_lot::Mutex;
+use std::convert::TryFrom;
+use std::future::Future;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::Context;
+use std::time::Duration;
+use wasm_bindgen::{closure::Closure, JsCast};
+
+use crate::{Instant, Timer, TimerHandle};
+
+/// Starts a background task, creates a `Timer`, and returns a handle to it.
+///
+/// > **Note**: Contrary to the original `futures-timer` crate, we don't have
+/// >           any `forget()` method, as the task is automatically considered
+/// >           as "forgotten".
+pub(crate) fn run() -> TimerHandle {
+    let timer = Timer::new();
+    let handle = timer.handle();
+    schedule_callback(Arc::new(Mutex::new(timer)), Duration::new(0, 0));
+    handle
+}
+
+/// Calls `Window::setTimeout` with the given `Duration`. The callback wakes up the timer and
+/// processes everything.
+fn schedule_callback(timer: Arc<Mutex<Timer>>, when: Duration) {
+    let window = web_sys::window().expect("Unable to access Window");
+    let _ = window
+        .set_timeout_with_callback_and_timeout_and_arguments_0(
+            &Closure::once_into_js(move || {
+                let mut timer_lock = timer.lock();
+
+                // We start by polling the timer. If any new `Delay` is created, the waker will be used
+                // to wake up this task pre-emptively. As such, we pass a `Waker` that calls
+                // `schedule_callback` with a delay of `0`.
+                let waker = task::waker(Arc::new(Waker {
+                    timer: timer.clone(),
+                }));
+                let _ = Future::poll(Pin::new(&mut *timer_lock), &mut Context::from_waker(&waker));
+
+                // Notify the timers that are ready.
+                let now = Instant::now();
+                timer_lock.advance_to(now);
+
+                // Each call to `schedule_callback` calls `schedule_callback` again, but also leaves
+                // the possibility for `schedule_callback` to be called in parallel. Since we don't
+                // want too many useless callbacks, we...
+                // TODO: ugh, that's a hack
+                if Arc::strong_count(&timer) > 20 {
+                    return;
+                }
+
+                // We call `schedule_callback` again for the next event.
+                let sleep_dur = timer_lock
+                    .next_event()
+                    .map(|next_event| {
+                        if next_event > now {
+                            next_event - now
+                        } else {
+                            Duration::new(0, 0)
+                        }
+                    })
+                    .unwrap_or(Duration::from_secs(5));
+                drop(timer_lock);
+                schedule_callback(timer, sleep_dur);
+            })
+            .unchecked_ref(),
+            i32::try_from(when.as_millis()).unwrap_or(0),
+        )
+        .unwrap();
+}
+
+struct Waker {
+    timer: Arc<Mutex<Timer>>,
+}
+
+impl ArcWake for Waker {
+    fn wake_by_ref(arc_self: &Arc<Self>) {
+        schedule_callback(arc_self.timer.clone(), Duration::new(0, 0));
+    }
+}
diff --git a/vendored-deps/wasm-timer/src/timer/heap.rs b/vendored-deps/wasm-timer/src/timer/heap.rs
new file mode 100644
index 000000000..f0f988229
--- /dev/null
+++ b/vendored-deps/wasm-timer/src/timer/heap.rs
@@ -0,0 +1,350 @@
+//! A simple binary heap with support for removal of arbitrary elements
+//!
+//! This heap is used to manage timer state in the event loop. All timeouts go
+//! into this heap and we also cancel timeouts from this heap. The crucial
+//! feature of this heap over the standard library's `BinaryHeap` is the ability
+//! to remove arbitrary elements. (e.g. when a timer is canceled)
+//!
+//! Note that this heap is not at all optimized right now, it should hopefully
+//! just work.
+
+use std::mem;
+
+pub struct Heap<T> {
+    // Binary heap of items, plus the slab index indicating what position in the
+    // list they're in.
+    items: Vec<(T, usize)>,
+
+    // A map from a slab index (assigned to an item above) to the actual index
+    // in the array the item appears at.
+    index: Vec<SlabSlot<usize>>,
+    next_index: usize,
+}
+
+enum SlabSlot<T> {
+    Empty { next: usize },
+    Full { value: T },
+}
+
+pub struct Slot {
+    idx: usize,
+}
+
+impl<T: Ord> Heap<T> {
+    pub fn new() -> Heap<T> {
+        Heap {
+            items: Vec::new(),
+            index: Vec::new(),
+            next_index: 0,
+        }
+    }
+
+    /// Pushes an element onto this heap, returning a slot token indicating
+    /// where it was pushed on to.
+    ///
+    /// The slot can later get passed to `remove` to remove the element from the
+    /// heap, but only if the element was previously not removed from the heap.
+    pub fn push(&mut self, t: T) -> Slot {
+        self.assert_consistent();
+        let len = self.items.len();
+        let slot = SlabSlot::Full { value: len };
+        let slot_idx = if self.next_index == self.index.len() {
+            self.next_index += 1;
+            self.index.push(slot);
+            self.index.len() - 1
+        } else {
+            match mem::replace(&mut self.index[self.next_index], slot) {
+                SlabSlot::Empty { next } => mem::replace(&mut self.next_index, next),
+                SlabSlot::Full { .. } => panic!(),
+            }
+        };
+        self.items.push((t, slot_idx));
+        self.percolate_up(len);
+        self.assert_consistent();
+        Slot { idx: slot_idx }
+    }
+
+    pub fn peek(&self) -> Option<&T> {
+        self.assert_consistent();
+        self.items.get(0).map(|i| &i.0)
+    }
+
+    pub fn pop(&mut self) -> Option<T> {
+        self.assert_consistent();
+        if self.items.len() == 0 {
+            return None;
+        }
+        let slot = Slot {
+            idx: self.items[0].1,
+        };
+        Some(self.remove(slot))
+    }
+
+    pub fn remove(&mut self, slot: Slot) -> T {
+        self.assert_consistent();
+        let empty = SlabSlot::Empty {
+            next: self.next_index,
+        };
+        let idx = match mem::replace(&mut self.index[slot.idx], empty) {
+            SlabSlot::Full { value } => value,
+            SlabSlot::Empty { .. } => panic!(),
+        };
+        self.next_index = slot.idx;
+        let (item, slot_idx) = self.items.swap_remove(idx);
+        debug_assert_eq!(slot.idx, slot_idx);
+        if idx < self.items.len() {
+            set_index(&mut self.index, self.items[idx].1, idx);
+            if self.items[idx].0 < item {
+                self.percolate_up(idx);
+            } else {
+                self.percolate_down(idx);
+            }
+        }
+        self.assert_consistent();
+        return item;
+    }
+
+    fn percolate_up(&mut self, mut idx: usize) -> usize {
+        while idx > 0 {
+            let parent = (idx - 1) / 2;
+            if self.items[idx].0 >= self.items[parent].0 {
+                break;
+            }
+            let (a, b) = self.items.split_at_mut(idx);
+            mem::swap(&mut a[parent], &mut b[0]);
+            set_index(&mut self.index, a[parent].1, parent);
+            set_index(&mut self.index, b[0].1, idx);
+            idx = parent;
+        }
+        return idx;
+    }
+
+    fn percolate_down(&mut self, mut idx: usize) -> usize {
+        loop {
+            let left = 2 * idx + 1;
+            let right = 2 * idx + 2;
+
+            let mut swap_left = true;
+            match (self.items.get(left), self.items.get(right)) {
+                (Some(left), None) => {
+                    if left.0 >= self.items[idx].0 {
+                        break;
+                    }
+                }
+                (Some(left), Some(right)) => {
+                    if left.0 < self.items[idx].0 {
+                        if right.0 < left.0 {
+                            swap_left = false;
+                        }
+                    } else if right.0 < self.items[idx].0 {
+                        swap_left = false;
+                    } else {
+                        break;
+                    }
+                }
+
+                (None, None) => break,
+                (None, Some(_right)) => panic!("not possible"),
+            }
+
+            let (a, b) = if swap_left {
+                self.items.split_at_mut(left)
+            } else {
+                self.items.split_at_mut(right)
+            };
+            mem::swap(&mut a[idx], &mut b[0]);
+            set_index(&mut self.index, a[idx].1, idx);
+            set_index(&mut self.index, b[0].1, a.len());
+            idx = a.len();
+        }
+        return idx;
+    }
+
+    fn assert_consistent(&self) {
+        if !cfg!(assert_timer_heap_consistent) {
+            return;
+        }
+
+        assert_eq!(
+            self.items.len(),
+            self.index
+                .iter()
+                .filter(|slot| {
+                    match **slot {
+                        SlabSlot::Full { .. } => true,
+                        SlabSlot::Empty { .. } => false,
+                    }
+                })
+                .count()
+        );
+
+        for (i, &(_, j)) in self.items.iter().enumerate() {
+            let index = match self.index[j] {
+                SlabSlot::Full { value } => value,
+                SlabSlot::Empty { .. } => panic!(),
+            };
+            if index != i {
+                panic!(
+                    "self.index[j] != i : i={} j={} self.index[j]={}",
+                    i, j, index
+                );
+            }
+        }
+
+        for (i, &(ref item, _)) in self.items.iter().enumerate() {
+            if i > 0 {
+                assert!(*item >= self.items[(i - 1) / 2].0, "bad at index: {}", i);
+            }
+            if let Some(left) = self.items.get(2 * i + 1) {
+                assert!(*item <= left.0, "bad left at index: {}", i);
+            }
+            if let Some(right) = self.items.get(2 * i + 2) {
+                assert!(*item <= right.0, "bad right at index: {}", i);
+            }
+        }
+    }
+}
+
+fn set_index<T>(slab: &mut Vec<SlabSlot<T>>, slab_slot: usize, val: T) {
+    match slab[slab_slot] {
+        SlabSlot::Full { ref mut value } => *value = val,
+        SlabSlot::Empty { .. } => panic!(),
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::Heap;
+
+    #[test]
+    fn simple() {
+        let mut h = Heap::new();
+        h.push(1);
+        h.push(2);
+        h.push(8);
+        h.push(4);
+        assert_eq!(h.pop(), Some(1));
+        assert_eq!(h.pop(), Some(2));
+        assert_eq!(h.pop(), Some(4));
+        assert_eq!(h.pop(), Some(8));
+        assert_eq!(h.pop(), None);
+        assert_eq!(h.pop(), None);
+    }
+
+    #[test]
+    fn simple2() {
+        let mut h = Heap::new();
+        h.push(5);
+        h.push(4);
+        h.push(3);
+        h.push(2);
+        h.push(1);
+        assert_eq!(h.pop(), Some(1));
+        h.push(8);
+        assert_eq!(h.pop(), Some(2));
+        h.push(1);
+        assert_eq!(h.pop(), Some(1));
+        assert_eq!(h.pop(), Some(3));
+        assert_eq!(h.pop(), Some(4));
+        h.push(5);
+        assert_eq!(h.pop(), Some(5));
+        assert_eq!(h.pop(), Some(5));
+        assert_eq!(h.pop(), Some(8));
+    }
+
+    #[test]
+    fn remove() {
+        let mut h = Heap::new();
+        h.push(5);
+        h.push(4);
+        h.push(3);
+        let two = h.push(2);
+        h.push(1);
+        assert_eq!(h.pop(), Some(1));
+        assert_eq!(h.remove(two), 2);
+        h.push(1);
+        assert_eq!(h.pop(), Some(1));
+        assert_eq!(h.pop(), Some(3));
+    }
+
+    fn vec2heap<T: Ord>(v: Vec<T>) -> Heap<T> {
+        let mut h = Heap::new();
+        for t in v {
+            h.push(t);
+        }
+        return h;
+    }
+
+    #[test]
+    fn test_peek_and_pop() {
+        let data = vec![2, 4, 6, 2, 1, 8, 10, 3, 5, 7, 0, 9, 1];
+        let mut sorted = data.clone();
+        sorted.sort();
+        let mut heap = vec2heap(data);
+        while heap.peek().is_some() {
+            assert_eq!(heap.peek().unwrap(), sorted.first().unwrap());
+            assert_eq!(heap.pop().unwrap(), sorted.remove(0));
+        }
+    }
+
+    #[test]
+    fn test_push() {
+        let mut heap = Heap::new();
+        heap.push(-2);
+        heap.push(-4);
+        heap.push(-9);
+        assert!(*heap.peek().unwrap() == -9);
+        heap.push(-11);
+        assert!(*heap.peek().unwrap() == -11);
+        heap.push(-5);
+        assert!(*heap.peek().unwrap() == -11);
+        heap.push(-27);
+        assert!(*heap.peek().unwrap() == -27);
+        heap.push(-3);
+        assert!(*heap.peek().unwrap() == -27);
+        heap.push(-103);
+        assert!(*heap.peek().unwrap() == -103);
+    }
+
+    fn check_to_vec(mut data: Vec<i32>) {
+        let mut heap = Heap::new();
+        for data in data.iter() {
+            heap.push(*data);
+        }
+        data.sort();
+        let mut v = Vec::new();
+        while let Some(i) = heap.pop() {
+            v.push(i);
+        }
+        assert_eq!(v, data);
+    }
+
+    #[test]
+    fn test_to_vec() {
+        check_to_vec(vec![]);
+        check_to_vec(vec![5]);
+        check_to_vec(vec![3, 2]);
+        check_to_vec(vec![2, 3]);
+        check_to_vec(vec![5, 1, 2]);
+        check_to_vec(vec![1, 100, 2, 3]);
+        check_to_vec(vec![1, 3, 5, 7, 9, 2, 4, 6, 8, 0]);
+        check_to_vec(vec![2, 4, 6, 2, 1, 8, 10, 3, 5, 7, 0, 9, 1]);
+        check_to_vec(vec![9, 11, 9, 9, 9, 9, 11, 2, 3, 4, 11, 9, 0, 0, 0, 0]);
+        check_to_vec(vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
+        check_to_vec(vec![10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0]);
+        check_to_vec(vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 0, 0, 1, 2]);
+        check_to_vec(vec![5, 4, 3, 2, 1, 5, 4, 3, 2, 1, 5, 4, 3, 2, 1]);
+    }
+
+    #[test]
+    fn test_empty_pop() {
+        let mut heap = Heap::<i32>::new();
+        assert!(heap.pop().is_none());
+    }
+
+    #[test]
+    fn test_empty_peek() {
+        let empty = Heap::<i32>::new();
+        assert!(empty.peek().is_none());
+    }
+}
diff --git a/vendored-deps/wasm-timer/src/timer/interval.rs b/vendored-deps/wasm-timer/src/timer/interval.rs
new file mode 100644
index 000000000..de201a1a0
--- /dev/null
+++ b/vendored-deps/wasm-timer/src/timer/interval.rs
@@ -0,0 +1,191 @@
+use pin_utils::unsafe_pinned;
+use std::pin::Pin;
+use std::task::{Context, Poll};
+use std::time::Duration;
+
+use futures::prelude::*;
+
+use crate::timer::delay;
+use crate::{Delay, Instant, TimerHandle};
+
+/// A stream representing notifications at fixed interval
+///
+/// Intervals are created through the `Interval::new` or
+/// `Interval::new_at` methods indicating when a first notification
+/// should be triggered and when it will be repeated.
+///
+/// Note that intervals are not intended for high resolution timers, but rather
+/// they will likely fire some granularity after the exact instant that they're
+/// otherwise indicated to fire at.
+#[derive(Debug)]
+pub struct Interval {
+    delay: Delay,
+    interval: Duration,
+}
+
+impl Interval {
+    unsafe_pinned!(delay: Delay);
+
+    /// Creates a new interval which will fire at `dur` time into the future,
+    /// and will repeat every `dur` interval after
+    ///
+    /// The returned object will be bound to the default timer for this thread.
+    /// The default timer will be spun up in a helper thread on first use.
+    pub fn new(dur: Duration) -> Interval {
+        Interval::new_at(Instant::now() + dur, dur)
+    }
+
+    /// Creates a new interval which will fire at the time specified by `at`,
+    /// and then will repeat every `dur` interval after
+    ///
+    /// The returned object will be bound to the default timer for this thread.
+    /// The default timer will be spun up in a helper thread on first use.
+    pub fn new_at(at: Instant, dur: Duration) -> Interval {
+        Interval {
+            delay: Delay::new_at(at),
+            interval: dur,
+        }
+    }
+
+    /// Creates a new interval which will fire at the time specified by `at`,
+    /// and then will repeat every `dur` interval after
+    ///
+    /// The returned object will be bound to the timer specified by `handle`.
+    pub fn new_handle(at: Instant, dur: Duration, handle: TimerHandle) -> Interval {
+        Interval {
+            delay: Delay::new_handle(at, handle),
+            interval: dur,
+        }
+    }
+}
+
+impl Stream for Interval {
+    type Item = ();
+
+    fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
+        if Pin::new(&mut *self).delay().poll(cx).is_pending() {
+            return Poll::Pending;
+        }
+        let next = next_interval(delay::fires_at(&self.delay), Instant::now(), self.interval);
+        self.delay.reset_at(next);
+        Poll::Ready(Some(()))
+    }
+}
+
+/// Converts Duration object to raw nanoseconds if possible
+///
+/// This is useful to divide intervals.
+///
+/// While technically for large duration it's impossible to represent any
+/// duration as nanoseconds, the largest duration we can represent is about
+/// 427_000 years. Large enough for any interval we would use or calculate in
+/// tokio.
+fn duration_to_nanos(dur: Duration) -> Option<u64> {
+    dur.as_secs()
+        .checked_mul(1_000_000_000)
+        .and_then(|v| v.checked_add(dur.subsec_nanos() as u64))
+}
+
+fn next_interval(prev: Instant, now: Instant, interval: Duration) -> Instant {
+    let new = prev + interval;
+    if new > now {
+        return new;
+    } else {
+        let spent_ns =
+            duration_to_nanos(now.duration_since(prev)).expect("interval should be expired");
+        let interval_ns =
+            duration_to_nanos(interval).expect("interval is less that 427 thousand years");
+        let mult = spent_ns / interval_ns + 1;
+        assert!(
+            mult < (1 << 32),
+            "can't skip more than 4 billion intervals of {:?} \
+             (trying to skip {})",
+            interval,
+            mult
+        );
+        return prev + interval * (mult as u32);
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use super::next_interval;
+    use std::time::{Duration, Instant};
+
+    struct Timeline(Instant);
+
+    impl Timeline {
+        fn new() -> Timeline {
+            Timeline(Instant::now())
+        }
+        fn at(&self, millis: u64) -> Instant {
+            self.0 + Duration::from_millis(millis)
+        }
+        fn at_ns(&self, sec: u64, nanos: u32) -> Instant {
+            self.0 + Duration::new(sec, nanos)
+        }
+    }
+
+    fn dur(millis: u64) -> Duration {
+        Duration::from_millis(millis)
+    }
+
+    // The math around Instant/Duration isn't 100% precise due to rounding
+    // errors, see #249 for more info
+    fn almost_eq(a: Instant, b: Instant) -> bool {
+        if a == b {
+            true
+        } else if a > b {
+            a - b < Duration::from_millis(1)
+        } else {
+            b - a < Duration::from_millis(1)
+        }
+    }
+
+    #[test]
+    fn norm_next() {
+        let tm = Timeline::new();
+        assert!(almost_eq(
+            next_interval(tm.at(1), tm.at(2), dur(10)),
+            tm.at(11)
+        ));
+        assert!(almost_eq(
+            next_interval(tm.at(7777), tm.at(7788), dur(100)),
+            tm.at(7877)
+        ));
+        assert!(almost_eq(
+            next_interval(tm.at(1), tm.at(1000), dur(2100)),
+            tm.at(2101)
+        ));
+    }
+
+    #[test]
+    fn fast_forward() {
+        let tm = Timeline::new();
+        assert!(almost_eq(
+            next_interval(tm.at(1), tm.at(1000), dur(10)),
+            tm.at(1001)
+        ));
+        assert!(almost_eq(
+            next_interval(tm.at(7777), tm.at(8888), dur(100)),
+            tm.at(8977)
+        ));
+        assert!(almost_eq(
+            next_interval(tm.at(1), tm.at(10000), dur(2100)),
+            tm.at(10501)
+        ));
+    }
+
+    /// TODO: this test actually should be successful, but since we can't
+    ///       multiply Duration on anything larger than u32 easily we decided
+    ///       to allow it to fail for now
+    #[test]
+    #[should_panic(expected = "can't skip more than 4 billion intervals")]
+    fn large_skip() {
+        let tm = Timeline::new();
+        assert_eq!(
+            next_interval(tm.at_ns(0, 1), tm.at_ns(25, 0), Duration::new(0, 2)),
+            tm.at_ns(25, 1)
+        );
+    }
+}
diff --git a/vendored-deps/wasm-timer/src/wasm.rs b/vendored-deps/wasm-timer/src/wasm.rs
new file mode 100644
index 000000000..87a88979f
--- /dev/null
+++ b/vendored-deps/wasm-timer/src/wasm.rs
@@ -0,0 +1,198 @@
+// Copyright 2019 Pierre Krieger
+// Copyright (c) 2019 Tokio Contributors
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the "Software"),
+// to deal in the Software without restriction, including without limitation
+// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+// and/or sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+// DEALINGS IN THE SOFTWARE.
+
+#![cfg(all(target_arch = "wasm32", target_os = "unknown"))]
+
+use std::cmp::{Eq, Ord, Ordering, PartialEq, PartialOrd};
+use std::ops::{Add, AddAssign, Sub, SubAssign};
+use std::time::Duration;
+
+#[derive(Debug, Copy, Clone)]
+pub struct Instant {
+    /// Unit is milliseconds.
+    inner: f64,
+}
+
+impl PartialEq for Instant {
+    fn eq(&self, other: &Instant) -> bool {
+        // Note that this will most likely only compare equal if we clone an `Instant`,
+        // but that's ok.
+        self.inner == other.inner
+    }
+}
+
+impl Eq for Instant {}
+
+impl PartialOrd for Instant {
+    fn partial_cmp(&self, other: &Instant) -> Option<Ordering> {
+        self.inner.partial_cmp(&other.inner)
+    }
+}
+
+impl Ord for Instant {
+    fn cmp(&self, other: &Self) -> Ordering {
+        self.inner.partial_cmp(&other.inner).unwrap()
+    }
+}
+
+impl Instant {
+    pub fn now() -> Instant {
+        let val = web_sys::window()
+            .expect("not in a browser")
+            .performance()
+            .expect("performance object not available")
+            .now();
+        Instant { inner: val }
+    }
+
+    pub fn duration_since(&self, earlier: Instant) -> Duration {
+        *self - earlier
+    }
+
+    pub fn elapsed(&self) -> Duration {
+        Instant::now() - *self
+    }
+}
+
+impl Add<Duration> for Instant {
+    type Output = Instant;
+
+    fn add(self, other: Duration) -> Instant {
+        let new_val = self.inner + other.as_millis() as f64;
+        Instant {
+            inner: new_val as f64,
+        }
+    }
+}
+
+impl Sub<Duration> for Instant {
+    type Output = Instant;
+
+    fn sub(self, other: Duration) -> Instant {
+        let new_val = self.inner - other.as_millis() as f64;
+        Instant {
+            inner: new_val as f64,
+        }
+    }
+}
+
+impl Sub<Instant> for Instant {
+    type Output = Duration;
+
+    fn sub(self, other: Instant) -> Duration {
+        let ms = self.inner - other.inner;
+        assert!(ms >= 0.0);
+        Duration::from_millis(ms as u64)
+    }
+}
+
+pub const UNIX_EPOCH: SystemTime = SystemTime { inner: 0.0 };
+
+#[derive(Debug, Copy, Clone)]
+pub struct SystemTime {
+    /// Unit is milliseconds.
+    inner: f64,
+}
+
+impl PartialEq for SystemTime {
+    fn eq(&self, other: &SystemTime) -> bool {
+        // Note that this will most likely only compare equal if we clone an `SystemTime`,
+        // but that's ok.
+        self.inner == other.inner
+    }
+}
+
+impl Eq for SystemTime {}
+
+impl PartialOrd for SystemTime {
+    fn partial_cmp(&self, other: &SystemTime) -> Option<Ordering> {
+        self.inner.partial_cmp(&other.inner)
+    }
+}
+
+impl Ord for SystemTime {
+    fn cmp(&self, other: &Self) -> Ordering {
+        self.inner.partial_cmp(&other.inner).unwrap()
+    }
+}
+
+impl SystemTime {
+    pub const UNIX_EPOCH: SystemTime = SystemTime { inner: 0.0 };
+
+    pub fn now() -> SystemTime {
+        let val = js_sys::Date::now();
+        SystemTime { inner: val }
+    }
+
+    pub fn duration_since(&self, earlier: SystemTime) -> Result<Duration, ()> {
+        let dur_ms = self.inner - earlier.inner;
+        if dur_ms < 0.0 {
+            return Err(());
+        }
+        Ok(Duration::from_millis(dur_ms as u64))
+    }
+
+    pub fn elapsed(&self) -> Result<Duration, ()> {
+        self.duration_since(SystemTime::now())
+    }
+
+    pub fn checked_add(&self, duration: Duration) -> Option<SystemTime> {
+        Some(*self + duration)
+    }
+
+    pub fn checked_sub(&self, duration: Duration) -> Option<SystemTime> {
+        Some(*self - duration)
+    }
+}
+
+impl Add<Duration> for SystemTime {
+    type Output = SystemTime;
+
+    fn add(self, other: Duration) -> SystemTime {
+        let new_val = self.inner + other.as_millis() as f64;
+        SystemTime {
+            inner: new_val as f64,
+        }
+    }
+}
+
+impl Sub<Duration> for SystemTime {
+    type Output = SystemTime;
+
+    fn sub(self, other: Duration) -> SystemTime {
+        let new_val = self.inner - other.as_millis() as f64;
+        SystemTime {
+            inner: new_val as f64,
+        }
+    }
+}
+
+impl AddAssign<Duration> for SystemTime {
+    fn add_assign(&mut self, rhs: Duration) {
+        *self = *self + rhs;
+    }
+}
+
+impl SubAssign<Duration> for SystemTime {
+    fn sub_assign(&mut self, rhs: Duration) {
+        *self = *self - rhs;
+    }
+}


Test patch:
diff --git a/bindings/ergo-lib-wasm/tests/integration_test_rest_api.js b/bindings/ergo-lib-wasm/tests/integration_test_rest_api.js
deleted file mode 100644
index b46c36e93..000000000
--- a/bindings/ergo-lib-wasm/tests/integration_test_rest_api.js
+++ /dev/null
@@ -1,26 +0,0 @@
-import { assert } from 'chai';
-
-import {
-  get_info, NodeConf,
-} from '../pkg/ergo_lib_wasm';
-
-// Note that REST calls fail without bringing in `Headers`, `Requests` and `Response` from the
-// `node-fetch` JS package. See https://rustwasm.github.io/wasm-pack/book/prerequisites/considerations.html
-import fetch from 'node-fetch';
-
-// @ts-ignore
-global.fetch = fetch;
-// @ts-ignore
-global.Headers = fetch.Headers;
-// @ts-ignore
-global.Request = fetch.Request;
-// @ts-ignore
-global.Response = fetch.Response;
-
-it('node REST API get_info endpoint', async () => {
-  let node_conf = new NodeConf("127.0.0.1:9053");
-  assert(node_conf != null);
-  let res = await get_info(node_conf);
-  assert(res != null);
-  assert(node_conf != null);
-});
diff --git a/bindings/ergo-lib-wasm/tests_browser/integration_tests_rest_api.js b/bindings/ergo-lib-wasm/tests_browser/integration_tests_rest_api.js
new file mode 100644
index 000000000..5004b5e14
--- /dev/null
+++ b/bindings/ergo-lib-wasm/tests_browser/integration_tests_rest_api.js
@@ -0,0 +1,18 @@
+// These integration tests assume that a local ergo node instance is running and its REST API is
+// accessible @ 127.0.0.1:9053.
+
+import { assert } from "chai";
+
+import * as ergo from "..";
+let ergo_wasm;
+beforeEach(async () => {
+    ergo_wasm = await ergo;
+});
+
+it('node REST API get_info endpoint', async () => {
+    let node_conf = new ergo_wasm.NodeConf("127.0.0.1:9053");
+    assert(node_conf != null);
+    let res = await ergo_wasm.get_info(node_conf);
+    assert(res != null);
+    assert(node_conf != null);
+});
\ No newline at end of file
diff --git a/bindings/ergo-lib-wasm/tests_browser/test_address.js b/bindings/ergo-lib-wasm/tests_browser/test_address.js
index bb1eff82d..8054aa548 100644
--- a/bindings/ergo-lib-wasm/tests_browser/test_address.js
+++ b/bindings/ergo-lib-wasm/tests_browser/test_address.js
@@ -22,4 +22,4 @@ it("Browser: P2S from base16 ergo tree", async () => {
   let tree = ergo_wasm.ErgoTree.from_base16_bytes(tree_bytes_base16_str);
   let addr = ergo_wasm.Address.recreate_from_ergo_tree(tree);
   assert(addr != null);
-});
+});
\ No newline at end of file
diff --git a/bindings/ergo-lib-wasm/tests_browser/test_rest_api.js b/bindings/ergo-lib-wasm/tests_browser/test_rest_api.js
new file mode 100644
index 000000000..e9519f87f
--- /dev/null
+++ b/bindings/ergo-lib-wasm/tests_browser/test_rest_api.js
@@ -0,0 +1,43 @@
+import { expect, assert } from "chai";
+import { generate_block_headers } from '../tests/utils';
+
+import * as ergo from "..";
+let ergo_wasm;
+beforeEach(async () => {
+    ergo_wasm = await ergo;
+});
+
+// Note that the REST API tests are here due to the WASM implementation of `reqwest-wrap`. In
+// particular the timeout functionality for HTTP requests requires the window object from the
+// web APIs, thus requiring a web browser to run.
+
+it('node REST API: get_nipopow_proof_by_header_id endpoint', async () => {
+    let node_conf = new ergo_wasm.NodeConf("213.239.193.208:9053");
+    assert(node_conf != null);
+    const block_headers = generate_block_headers();
+    const header_id = block_headers.get(0).id();
+    let res = await ergo_wasm.get_nipopow_proof_by_header_id(node_conf, 3, 4, header_id);
+    assert(res != null);
+    assert(node_conf != null);
+});
+
+it('node REST API: peer_discovery endpoint', async () => {
+    const seeds = [
+        "http://213.239.193.208:9030",
+        "http://159.65.11.55:9030",
+        "http://165.227.26.175:9030",
+        "http://159.89.116.15:9030",
+        "http://136.244.110.145:9030",
+        "http://94.130.108.35:9030",
+        "http://51.75.147.1:9020",
+        "http://221.165.214.185:9030",
+        "http://51.81.185.231:9031",
+        "http://217.182.197.196:9030",
+        "http://62.171.190.193:9030",
+        "http://173.212.220.9:9030",
+        "http://176.9.65.58:9130",
+        "http://213.152.106.56:9030",
+    ].map(x => new URL(x));
+    let res = await ergo_wasm.peer_discovery(seeds, 10, 3);
+    assert(res.len() > 0, "Should be at least one peer!");
+});
diff --git a/vendored-deps/reqwest-wrap/tests/wasm_simple.rs b/vendored-deps/reqwest-wrap/tests/wasm_simple.rs
new file mode 100644
index 000000000..870afe572
--- /dev/null
+++ b/vendored-deps/reqwest-wrap/tests/wasm_simple.rs
@@ -0,0 +1,41 @@
+#![cfg(target_arch = "wasm32")]
+#![allow(clippy::unused_unit)]
+
+use reqwest_wrap as reqwest;
+use std::time::Duration;
+use wasm_bindgen::prelude::*;
+use wasm_bindgen_test::*;
+wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
+
+#[wasm_bindgen]
+extern "C" {
+    // Use `js_namespace` here to bind `console.log(..)` instead of just
+    // `log(..)`
+    #[wasm_bindgen(js_namespace = console)]
+    fn log(s: &str);
+
+}
+#[wasm_bindgen_test]
+async fn simple_example() {
+    let res = reqwest::get("https://hyper.rs")
+        .await
+        .expect("http get example");
+
+    log(&format!("Status: {}", res.status()));
+
+    let body = res.text().await.expect("response to utf-8 text");
+    log(&format!("Body:\n\n{}", body));
+}
+
+#[wasm_bindgen_test]
+async fn client_with_timeout() {
+    let client = reqwest::Client::new();
+    let err = client
+        .get("https://hyper.rs")
+        .timeout(Duration::from_millis(10))
+        .send()
+        .await
+        .expect_err("Expected error from aborted request");
+
+    assert!(err.is_request());
+}