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

Issue:
[500 SigmaUSD] `peer_discovery` REST API has problems on Chrome
The current implementation of `peer_discovery` doesn't quite work on Chrome (it works perfectly on Firefox and tokio). Basically it cannot discover anywhere near the amount of existing active nodes (it mostly finds 0 or 1). I tried my best to find the underlying cause, but no dice. Although the function returns without error, there is also a really bad bug that results: after calling `peer_discovery` any subsequent HTTP requests fail, with `reqwest` reporting that the request times out.

## Mitigation
I tweaked the value of many parameters and found a mitigation for this. My educated guess from these experiments is that Chrome has an issue where it can't handle too many concurrent promises. So referring to my schematic from the code, `peer_discovery` works as follows:

 - 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>   
```

The solution for Chrome is to restrict the number of URLs sent to task 2. More specifically we must make the following changes:
 - The global timeout interval is key. For chrome this needs to be **no more than 1.5 seconds**. Anything longer and the bug described above appears.
 - The timeout isn't strict anymore. When it triggers we wait for any in-flight URLs to be checked, then we return.
 - The queue that task 2 uses to buffer incoming URLs is set to length of 50.
 - Surprisingly, the timeout interval for each individual HTTP request to each peer node needs to be about 6 seconds (this even affects Firefox). Lower values results in missing some active nodes. This problem doesn't occur with the `tokio` version: things work fine setting the interval to 2 seconds.

Now what this means is that Chrome can't discover all the available nodes (it can get about 13). The following table gives rough results by timeout length when using Firefox:

|Global timeout(sec)| # peers reached| # active peers discovered| 
|--|---|--|
|[1.0, 4.4] | ~260 | ~13|
| [4.5, ..) | ~560| ~16|

The results are like a step function. For example any interval between 1 to 4.4 seconds gives us 13 active nodes.


Discussion / hints:
Thank you for the very thorough explanation! Sounds good! I'm adding bounty for this task.

>     * The queue that task 2 uses to buffer incoming URLs is set to length of 50.

I suppose this buffer(50) is essentially the limit of concurrent requests, right? So we could fine-tune it later on.


> I'm adding bounty for this task.

Thank you!

> I suppose this buffer(50) is essentially the limit of concurrent requests, right? So we could fine-tune it later on.

The buffer smooths the flow of data between the 2 tasks, which is interacting in a tight loop. If the buffer is too small, task 2 can't get URLs-to-check fast enough which in turn clogs up task 1 as it will accumulate too many `peer` URLs. I've been running more tests and 50 might even be a bit large (performance plateaus past a certain size).

It looks like the browsers are now capable of massive concurrent requests, Chrome can do 256 whilst Firefox is > 1000 (according to tests described here: https://stackoverflow.com/a/30064610). Now to date there's 616 ergo nodes out there, so we can go beyond Chrome's limit. But I throttled the max requests <= 256 and the bug still persists on Chrome, so the problem lies elsewhere.

I'll dig into this some more and I'll consolidate all the parameters into a struct for easy fine-tuning.

Got it. Thank you!

Fix patch:
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 7cd7cf121..a44e086d6 100644
--- a/bindings/ergo-lib-c-core/src/rest/api/node.rs
+++ b/bindings/ergo-lib-c-core/src/rest/api/node.rs
@@ -176,7 +176,9 @@ pub unsafe fn rest_api_node_peer_discovery(
             Ok(peers) => {
                 let length = peers.len();
                 let mut out = vec![];
-                for peer in peers {
+                for mut peer in peers {
+                    #[allow(clippy::unwrap_used)]
+                    peer.set_port(Some(9053)).unwrap();
                     out.push(CString::new(peer.as_str()).unwrap().into_raw());
                 }
 
diff --git a/bindings/ergo-lib-wasm/karma.conf.js b/bindings/ergo-lib-wasm/karma.conf.js
index 9e0370ba1..1faaa8c75 100644
--- a/bindings/ergo-lib-wasm/karma.conf.js
+++ b/bindings/ergo-lib-wasm/karma.conf.js
@@ -11,7 +11,7 @@ module.exports = function (config) {
     ],
     client: {
       mocha: {
-        timeout: 60000
+        timeout: 900000
       }
     },
     webpack: {
@@ -58,7 +58,7 @@ module.exports = function (config) {
     logLevel: config.LOG_INFO,
 
     browsers: ["ChromeHeadless"],
-    browserNoActivityTimeout: 60000,
+    browserNoActivityTimeout: 900000,
 
     autoWatch: false,
     singleRun: true,
diff --git a/bindings/ergo-lib-wasm/package.json b/bindings/ergo-lib-wasm/package.json
index 4122b87d1..81572f24e 100644
--- a/bindings/ergo-lib-wasm/package.json
+++ b/bindings/ergo-lib-wasm/package.json
@@ -40,4 +40,4 @@
     "dependencies": {
         "babel": "^6.23.0"
     }
-}
\ No newline at end of file
+}
diff --git a/bindings/ergo-lib-wasm/src/rest.rs b/bindings/ergo-lib-wasm/src/rest.rs
index 30f62daf2..ca160a166 100644
--- a/bindings/ergo-lib-wasm/src/rest.rs
+++ b/bindings/ergo-lib-wasm/src/rest.rs
@@ -1,5 +1,8 @@
 //! Wasm API for ergo-rest crate
 
+#[cfg(target_arch = "wasm32")]
 pub mod api;
+#[cfg(target_arch = "wasm32")]
+pub mod chrome_peer_discovery_scan;
 pub mod node_conf;
 pub mod node_info;
diff --git a/bindings/ergo-lib-wasm/src/rest/api.rs b/bindings/ergo-lib-wasm/src/rest/api.rs
index 468eb7ccb..3a92bc0ae 100644
--- a/bindings/ergo-lib-wasm/src/rest/api.rs
+++ b/bindings/ergo-lib-wasm/src/rest/api.rs
@@ -108,7 +108,7 @@ pub fn get_blocks_header_id_proof_for_tx_id(
 /// 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>);
+pub struct PeerUrls(pub(crate) Vec<web_sys::Url>);
 
 #[wasm_bindgen]
 impl PeerUrls {
@@ -125,16 +125,35 @@ impl PeerUrls {
 
 /// 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
+///  - `max_parallel_tasks` represents the maximum number of tasks to spawn for ergo node HTTP
+///    requests. Note that the actual number of parallel HTTP requests may well be higher than this
+///    number.
 ///  - `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.
+///  - `is_chrome` **MUST** be set to true if running this function on a Chromium-based browser.
+///    There are some limitations on this platform regarding network requests. Please see the
+///    documentation for [`peer_discovery_chrome`].
 #[wasm_bindgen]
 pub async fn peer_discovery(
     seeds: Box<[web_sys::Url]>,
     max_parallel_requests: u16,
     timeout_sec: u32,
+    is_chrome: bool,
+) -> Result<PeerUrls, JsValue> {
+    if is_chrome {
+        peer_discovery_chrome(seeds, max_parallel_requests, timeout_sec).await
+    } else {
+        peer_discovery_non_chrome(seeds, max_parallel_requests, timeout_sec).await
+    }
+}
+
+/// IMPORTANT: do not call this function on Chromium, as it will likely mess with the browser's
+/// ability to make HTTP requests. Use `peer_discovery_chrome` instead.
+async fn peer_discovery_non_chrome(
+    seeds: Box<[web_sys::Url]>,
+    max_parallel_requests: u16,
+    timeout_sec: u32,
 ) -> Result<PeerUrls, JsValue> {
     let mut converted_seeds = vec![];
     for seed in &*seeds {
@@ -150,8 +169,99 @@ pub async fn peer_discovery(
         .await
         .map_err(to_js)?;
     let mut peer_urls = vec![];
-    for url in res {
+    for mut url in res {
+        #[allow(clippy::unwrap_used)]
+        url.set_port(Some(9053)).unwrap();
+        peer_urls.push(web_sys::Url::new(url.as_str())?);
+    }
+    Ok(PeerUrls(peer_urls))
+}
+
+/// 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. It's not possible to give a definitive upper bound
+///  - `timeout` represents the amount of time that is spent searching for peers PLUS a waiting
+///    period of 80 seconds to give Chrome the time to relinquish failed preflight requests. Must be
+///    at least 90 seconds. Once the timeout value is reached, return with the vec of active peers
+///    that have been discovered up to that point in time.
+///
+/// NOTE: intended to be used only on Chromium based browsers. It works on Firefox and Safari, but
+/// using `peer_discovery` above gives better performance. Why? See below.
+///
+/// ## Technical details
+/// Chrome has a problem with hanging on to [`preflight`] requests where the server does not
+/// respond. Every browser request we make of an ergo node will be preceded by a preflight request.
+/// These preflight requests are automatically initiated by the browser, and it is not possible for
+/// us to interact with it.
+//
+/// If a server doesn't respond to any request, then Chrome waits on the preflight request from
+/// anywhere from 1.3 to 2.4 minutes. Chrome also doesn't have a high ceiling for parallel requests
+/// and running our first implementation of `peer_discovery` in the `non_chrome` submodule brings
+/// Chrome to a halt, preventing it from making any further requests until enough of the preflights
+/// have been marked as failed. This is quite unfortunate as Firefox and Safari almost immediately
+/// drops such preflight requests. See [`this issue`] for more discussion and example runs.
+//
+/// So this implementation is largely the same as in `non_chrome` except that we throttle the number
+/// of parallel requests made. The majority of ergo nodes do not respond to REST requests, so there
+/// will be a large number of preflight requests 'taking up space' and dramatically reducing
+/// throughput.
+async fn peer_discovery_chrome(
+    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_chrome(
+        seeds,
+        max_parallel_requests,
+        timeout,
+    )
+    .await
+    .map_err(to_js)?;
+    let mut peer_urls = vec![];
+    for mut url in res {
+        #[allow(clippy::unwrap_used)]
+        url.set_port(Some(9053)).unwrap();
         peer_urls.push(web_sys::Url::new(url.as_str())?);
     }
     Ok(PeerUrls(peer_urls))
 }
+
+/// An incremental (reusable) version of [`peer_discovery_chrome`] which allows for peer discovery
+/// to be split into separate sub-tasks.
+///
+/// NOTE: intended to be used only on Chromium based browsers. It works on Firefox and Safari, but
+/// using `peer_discovery` above gives better performance.
+#[wasm_bindgen]
+pub fn incremental_peer_discovery_chrome(
+    scan: &super::chrome_peer_discovery_scan::ChromePeerDiscoveryScan,
+    max_parallel_requests: u16,
+    timeout_sec: u32,
+) -> js_sys::Promise {
+    let scan_cloned = scan.clone();
+    wasm_bindgen_futures::future_to_promise(async move {
+        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 updated_scan = ergo_lib::ergo_rest::api::node::incremental_peer_discovery_chrome(
+            scan_cloned.0,
+            max_parallel_requests,
+            timeout,
+        )
+        .await
+        .map_err(to_js)
+        .map(super::chrome_peer_discovery_scan::ChromePeerDiscoveryScan::from)?;
+        Ok(wasm_bindgen::JsValue::from(updated_scan))
+    })
+}
diff --git a/bindings/ergo-lib-wasm/src/rest/chrome_peer_discovery_scan.rs b/bindings/ergo-lib-wasm/src/rest/chrome_peer_discovery_scan.rs
new file mode 100644
index 000000000..03bba827d
--- /dev/null
+++ b/bindings/ergo-lib-wasm/src/rest/chrome_peer_discovery_scan.rs
@@ -0,0 +1,41 @@
+//! A Chrome peer discovery scan stores the results of potentially-multiple calls to
+//! `peer_discovery_chrome`. It allows the user the ability to break up peer discovery into smaller
+//! sub-tasks.
+
+use super::api::PeerUrls;
+use crate::error_conversion::to_js;
+use bounded_vec::NonEmptyVec;
+use derive_more::{From, Into};
+use wasm_bindgen::prelude::*;
+
+/// Node info
+#[wasm_bindgen]
+#[derive(Debug, Clone, From, Into)]
+pub struct ChromePeerDiscoveryScan(
+    pub(crate) ergo_lib::ergo_rest::api::node::ChromePeerDiscoveryScan,
+);
+
+#[wasm_bindgen]
+impl ChromePeerDiscoveryScan {
+    /// Create new scan. Note: `seeds` must not be empty.
+    #[wasm_bindgen(constructor)]
+    pub fn new(seeds: Box<[web_sys::Url]>) -> Result<ChromePeerDiscoveryScan, 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)?;
+        Ok(ergo_lib::ergo_rest::api::node::ChromePeerDiscoveryScan::new(seeds).into())
+    }
+
+    /// Returns list of non-seed peers with an active REST API.
+    pub fn active_peers(&self) -> Result<PeerUrls, JsValue> {
+        let mut peer_urls = vec![];
+        for mut url in self.0.active_peers() {
+            url.set_port(Some(9053)).unwrap();
+            peer_urls.push(web_sys::Url::new(url.as_str())?);
+        }
+        Ok(PeerUrls(peer_urls))
+    }
+}
diff --git a/bindings/ergo-lib-wasm/src/rest/node_conf.rs b/bindings/ergo-lib-wasm/src/rest/node_conf.rs
index f652e5472..8a36bc34a 100644
--- a/bindings/ergo-lib-wasm/src/rest/node_conf.rs
+++ b/bindings/ergo-lib-wasm/src/rest/node_conf.rs
@@ -17,10 +17,10 @@ pub struct NodeConf(pub(crate) ergo_lib::ergo_rest::NodeConf);
 #[wasm_bindgen]
 impl NodeConf {
     /// Create a node configuration
-    /// addr - a string in a format 'ip_address:port'
     #[wasm_bindgen(constructor)]
-    pub fn new(addr: &str) -> Result<NodeConf, JsValue> {
-        let peer_addr = PeerAddr::from_str(addr).map_err(to_js)?;
+    pub fn new(url: &web_sys::Url) -> Result<NodeConf, JsValue> {
+        // host string must be in format 'ip_address:port'
+        let peer_addr = PeerAddr::from_str(&url.host()).map_err(to_js)?;
         Ok(ergo_lib::ergo_rest::NodeConf {
             addr: peer_addr,
             api_key: None,
diff --git a/ergo-rest/Cargo.toml b/ergo-rest/Cargo.toml
index 71d06efa4..0b29fae76 100644
--- a/ergo-rest/Cargo.toml
+++ b/ergo-rest/Cargo.toml
@@ -27,6 +27,7 @@ futures = "0.3"
 thiserror = "1"
 derive_more = "0.99"
 proptest-derive = {version = "0.3.0", optional = true }
+rand = "0.8.4"
 serde = { version = "1.0", features = ["derive"]}
 serde_json = { version = "1.0", optional = true }
 url = "2.2"
@@ -45,10 +46,12 @@ js-sys = "0.3"
 wasm-bindgen = "0.2"
 # Depdencies for `reqwest`
 serde_json = "1.0"
+getrandom = { version = "0.2", features = ["js"] }
 
 [target.'cfg(target_arch = "wasm32")'.dependencies.web-sys]
 version = "0.3.25"
 features = [
+    "console",
     "Headers",
     "Request",
     "RequestInit",
@@ -56,6 +59,7 @@ features = [
     "AbortController",
     "AbortSignal",
     "Response",
+    "ReadableStream",
     "Window",
     "FormData",
     "Blob",
diff --git a/ergo-rest/src/api/node.rs b/ergo-rest/src/api/node.rs
index cd4ee6d5a..3e683fac6 100644
--- a/ergo-rest/src/api/node.rs
+++ b/ergo-rest/src/api/node.rs
@@ -18,6 +18,9 @@ use crate::NodeInfo;
 use super::build_client;
 use super::set_req_headers;
 
+#[cfg(target_arch = "wasm32")]
+pub use crate::api::peer_discovery_internals::ChromePeerDiscoveryScan;
+
 /// GET on /info endpoint
 pub async fn get_info(node: NodeConf) -> Result<NodeInfo, NodeError> {
     #[allow(clippy::unwrap_used)]
@@ -50,18 +53,68 @@ pub async fn get_header(node: NodeConf, header_id: BlockId) -> Result<Header, No
 
 /// 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
+///  - `max_parallel_tasks` represents the maximum number of tasks to spawn for ergo node HTTP
+///    requests. Note that the actual number of parallel HTTP requests may well be higher than this
+///    number.
 ///  - `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.
+///
+/// IMPORTANT: do not call this function on Chromium, as it will likely mess with the browser's
+/// ability to make HTTP requests. Use `peer_discovery_chrome` instead. For more information why
+/// please refer to the module documentation for `crate::api::peer_discovery_internals::chrome`.
 pub async fn peer_discovery(
+    seeds: NonEmptyVec<Url>,
+    max_parallel_tasks: BoundedU16<1, { u16::MAX }>,
+    timeout: Duration,
+) -> Result<Vec<Url>, PeerDiscoveryError> {
+    super::peer_discovery_internals::peer_discovery_inner(seeds, max_parallel_tasks, timeout).await
+}
+
+#[cfg(target_arch = "wasm32")]
+/// 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 searching for peers PLUS a waiting
+///    period of 80 seconds to give Chrome the time to relinquish failed preflight requests. Must be
+///    at least 90 seconds. Once the timeout value is reached, return with the vec of active peers
+///    that have been discovered up to that point in time.
+///
+/// NOTE: intended to be used only on Chromium based browsers. It works on Firefox and Safari, but
+/// using `peer_discovery` above gives better performance.
+pub async fn peer_discovery_chrome(
     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
+    let scan = super::peer_discovery_internals::ChromePeerDiscoveryScan::new(seeds);
+    super::peer_discovery_internals::peer_discovery_inner_chrome(
+        scan,
+        max_parallel_requests,
+        timeout,
+    )
+    .await
+    .map(|scan| scan.active_peers())
+}
+
+#[cfg(target_arch = "wasm32")]
+/// An incremental (reusable) version of [`peer_discovery_chrome`] which allows for peer discovery
+/// to be split into separate sub-tasks.
+///
+/// NOTE: intended to be used only on Chromium based browsers. It works on Firefox and Safari, but
+/// using `peer_discovery` above gives better performance.
+pub async fn incremental_peer_discovery_chrome(
+    scan: ChromePeerDiscoveryScan,
+    max_parallel_requests: BoundedU16<1, { u16::MAX }>,
+    timeout: Duration,
+) -> Result<ChromePeerDiscoveryScan, PeerDiscoveryError> {
+    super::peer_discovery_internals::peer_discovery_inner_chrome(
+        scan,
+        max_parallel_requests,
+        timeout,
+    )
+    .await
 }
 
 /// GET on /nipopow/proof/{minChainLength}/{suffixLength}/{headerId} endpoint
@@ -74,7 +127,7 @@ pub async fn get_nipopow_proof_by_header_id(
     if min_chain_length == 0 || suffix_len == 0 {
         return Err(NodeError::InvalidNumericalUrlSegment);
     }
-    let header_str = String::from(header_id.0);
+    let header_str = String::from(header_id.0.clone());
     let mut path = "nipopow/proof/".to_owned();
     path.push_str(&*min_chain_length.to_string());
     path.push('/');
@@ -202,7 +255,7 @@ mod tests {
             let res_quick = peer_discovery(
                 NonEmptyVec::from_vec(seeds.clone()).unwrap(),
                 BoundedU16::new(5).unwrap(),
-                Duration::from_millis(2010),
+                Duration::from_millis(1000),
             )
             .await
             .unwrap();
@@ -218,6 +271,11 @@ mod tests {
             .unwrap();
             (res_quick, res_long)
         });
+        println!(
+            "{} quick peers, {} long peers",
+            res_with_quick_timeout.len(),
+            res_with_longer_timeout.len()
+        );
         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
index 7c0a1b459..3bdf31cf7 100644
--- a/ergo-rest/src/api/peer_discovery_internals.rs
+++ b/ergo-rest/src/api/peer_discovery_internals.rs
@@ -1,301 +1,23 @@
-//! 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 _ = crate::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),
-                    };
+//! This module contains the `peer_discovery` implementation. The original implementation is found
+//! in the `non_chrome` sub-module, which can be used on tokio as well as web-browsers that are not
+//! Chrome (tested on Firefox and Safari). However there are certain limitations in Chrome that
+//! require us to have a custom implementation.
+#[cfg(target_arch = "wasm32")]
+mod chrome;
+mod non_chrome;
 
-                    // 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
+use bounded_integer::BoundedU16;
+use std::time::Duration;
 
-    // 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")]
+pub(crate) use chrome::peer_discovery_inner_chrome;
+#[cfg(target_arch = "wasm32")]
+pub use chrome::ChromePeerDiscoveryScan;
+pub(crate) use non_chrome::peer_discovery_inner;
 
-    #[cfg(target_arch = "wasm32")]
-    let spawn_fn_new = wasm_bindgen_futures::spawn_local;
+use crate::{NodeConf, NodeError, PeerInfo};
 
-    // (*) Run stream to completion.
-    spawn_fn_new(mapped_stream.for_each(|_| async move {}));
-}
+use super::{build_client, set_req_headers};
 
 /// GET on /peers/all endpoint
 async fn get_peers_all(node: NodeConf) -> Result<Vec<PeerInfo>, NodeError> {
@@ -307,104 +29,9 @@ async fn get_peers_all(node: NodeConf) -> Result<Vec<PeerInfo>, NodeError> {
     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())
-    }
+struct PeerDiscoverySettings {
+    max_parallel_tasks: BoundedU16<1, { u16::MAX }>,
+    task_2_buffer_length: usize,
+    global_timeout: Duration,
+    timeout_of_individual_node_request: Duration,
 }
diff --git a/ergo-rest/src/api/peer_discovery_internals/chrome.rs b/ergo-rest/src/api/peer_discovery_internals/chrome.rs
new file mode 100644
index 000000000..eeea1af36
--- /dev/null
+++ b/ergo-rest/src/api/peer_discovery_internals/chrome.rs
@@ -0,0 +1,575 @@
+//! Chrome implementation of `peer_discovery`.
+//!
+//! Why? Chrome has a problem with hanging on to [`preflight`] requests where the server does not
+//! respond. Every browser request we make of an ergo node will be preceded by a preflight request.
+//! These preflight requests are automatically initiated by the browser, and it is not possible for
+//! us to interact with it.
+//!
+//! If a server doesn't respond to any request, then Chrome waits on the preflight request from
+//! anywhere from 1.3 to 2.4 minutes. Chrome also doesn't have a high ceiling for parallel requests
+//! and running our first implementation of `peer_discovery` in the `non_chrome` submodule brings
+//! Chrome to a halt, preventing it from making any further requests until enough of the preflights
+//! have been marked as failed. This is quite unfortunate as Firefox and Safari almost immediately
+//! drops such preflight requests. See [`this issue`] for more discussion and example runs.
+//!
+//! So this implementation is largely the same as in `non_chrome` except that we throttle the number
+//! of parallel requests made. The majority of ergo nodes do not respond to REST requests, so there
+//! will be a large number of preflight requests 'taking up space' and dramatically reducing
+//! throughput.
+//!
+//! # Differences with the original implementation in `non_chrome`
+//! - Fine-grained counting and throttling of the number of active parallel requests. It's not
+//!   perfect; the preflights are unobservable but we can simulate when they end based on empirical
+//!   data.
+//! - Task 2 no longer receives URLs but rather specific node requests to be made (either for /info
+//!   or /peers/all endpoints)
+//! - Instead of `peer_stack` of URLs in task 1, we use a priority queue so that any requests to
+//!   /peers/all is pushed through first.
+//!
+//! [`preflight`]: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#preflighted_requests
+//! [`this issue`]: https://github.com/ergoplatform/sigma-rust/issues/581#issuecomment-1160564378
+use super::PeerDiscoverySettings;
+use crate::api::peer_discovery_internals::get_peers_all;
+use crate::error::PeerDiscoveryError;
+use crate::{api::node::get_info, NodeConf, NodeError, PeerInfo};
+use bounded_integer::BoundedU16;
+use bounded_vec::NonEmptyVec;
+use ergo_chain_types::PeerAddr;
+use std::fmt::Debug;
+use std::{
+    collections::{BinaryHeap, HashSet},
+    time::Duration,
+};
+use url::Url;
+
+// Uncomment the following to enable logging on WASM through the `console_log` macro. Taken from
+// https://rustwasm.github.io/wasm-bindgen/examples/console-log.html#srclibrs
+//use wasm_bindgen::prelude::*;
+//
+//#[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);
+//}
+//
+//macro_rules! console_log {
+//// Note that this is using the `log` function imported above during
+//// `bare_bones`
+//($($t:tt)*) => (log(&format_args!($($t)*).to_string()))
+//}
+
+pub(crate) async fn peer_discovery_inner_chrome(
+    scan: ChromePeerDiscoveryScan,
+    max_parallel_tasks: BoundedU16<1, { u16::MAX }>,
+    timeout: Duration,
+) -> Result<ChromePeerDiscoveryScan, PeerDiscoveryError> {
+    if timeout.as_secs() < 90 {
+        return Err(PeerDiscoveryError::TimeoutTooShort);
+    }
+
+    // Note that 80 seconds is allocated to waiting for preflight requests to naturally timeout by
+    // Chrome. The remaining time is spent looking for peers.
+    let global_timeout = timeout.checked_sub(Duration::from_secs(80)).unwrap();
+    let settings = PeerDiscoverySettings {
+        max_parallel_tasks,
+        task_2_buffer_length: 50,
+        global_timeout,
+        timeout_of_individual_node_request: Duration::from_secs(6),
+    };
+
+    let (tx_msg, rx_msg) = futures::channel::mpsc::channel::<Msg>(settings.task_2_buffer_length);
+    let (tx_node_request, rx_node_request) =
+        futures::channel::mpsc::channel::<NodeRequest>(settings.task_2_buffer_length);
+    let node_request_stream = rx_node_request;
+    let msg_stream = rx_msg;
+
+    peer_discovery_impl_chrome(
+        scan,
+        tx_msg,
+        msg_stream,
+        tx_node_request,
+        node_request_stream,
+        settings,
+    )
+    .await
+}
+
+/// Implementation of `peer_discovery`.
+async fn peer_discovery_impl_chrome(
+    scan: ChromePeerDiscoveryScan,
+    tx_msg: futures::channel::mpsc::Sender<Msg>,
+    msg_stream: futures::channel::mpsc::Receiver<Msg>,
+    mut tx_node_request: futures::channel::mpsc::Sender<NodeRequest>,
+    node_request_stream: futures::channel::mpsc::Receiver<NodeRequest>,
+    settings: PeerDiscoverySettings,
+) -> Result<ChromePeerDiscoveryScan, PeerDiscoveryError> {
+    use futures::future::FutureExt;
+    use futures::{SinkExt, StreamExt};
+
+    let max_parallel_requests = settings.max_parallel_tasks.get() as usize;
+
+    let ChromePeerDiscoveryScan {
+        active_peers,
+        mut visited_peers,
+        seeds_set,
+        mut pending_requests,
+    } = scan;
+
+    let mut active_peers: HashSet<Url> = active_peers.into_iter().collect();
+
+    // Task 2 from the schematic above
+    spawn_http_request_task_chrome(
+        tx_msg,
+        node_request_stream,
+        settings.max_parallel_tasks,
+        settings.timeout_of_individual_node_request,
+    );
+
+    // Push through a single request to start the workflow
+    if let Some(node_request) = pending_requests.pop() {
+        tx_node_request
+            .send(node_request)
+            .await
+            .map_err(|_| PeerDiscoveryError::MpscSender)?;
+    } else {
+        return Err(PeerDiscoveryError::NoPendingNodeRequests);
+    }
+
+    // (*) 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 and `chrome_request_count` reaches zero we break the loop below. This leads us to
+    // drop `tx_node_request`, which is the sender side of the receiver stream
+    // `node_request_stream`, allowing task 1 to end.
+    let mut count = 1;
+
+    // This variable tracks the number of active requests opened by Chrome. Every request we make of
+    // an ergo node requires a 'preflight' request first. This variable tracks such requests too.
+    // It's used to restrict the total number of active requests on Chrome.
+    let mut chrome_request_count = 2;
+
+    // Represents pending NodeRequests that cannot be made since they've come in after the gobal
+    // timeout duration.
+    let mut pending_requests_after_timeout = BinaryHeap::new();
+
+    // Here we spawn a task that triggers a signal after `settings.global_timeout` has elapsed.
+    let rx_timeout_signal = {
+        let (tx, rx) = futures::channel::oneshot::channel::<()>();
+        wasm_bindgen_futures::spawn_local(async move {
+            crate::wasm_timer::Delay::new(settings.global_timeout)
+                .await
+                .expect("wasm_timer::Delay: can't spawn global timeout");
+            tx.send(()).unwrap();
+        });
+        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);
+
+    // This variable equals to true as long as we're checking for new peer nodes. It is set to false
+    // once the global timeout is reached.
+    let mut add_peers = true;
+    '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_node_request, rx_node_request) channel
+                while let Some(peer) = pending_requests.pop() {
+                    let mut url = peer.get_url().clone();
+                    #[allow(clippy::unwrap_used)]
+                    url.set_port(None).unwrap();
+                    let room_for_requests = (max_parallel_requests > chrome_request_count)
+                        && max_parallel_requests - chrome_request_count >= 2;
+                    let peers_all = if let NodeRequest::PeersAll(_) = peer {
+                        true
+                    } else {
+                        false
+                    };
+                    if peers_all || !visited_peers.contains(&url) {
+                        if room_for_requests {
+                            match tx_node_request.try_send(peer.clone()) {
+                                Ok(_) => {
+                                    chrome_request_count += 2;
+                                    if !peers_all {
+                                        count += 1;
+                                    }
+                                    //console_log!(
+                                    //    "Adding {}. count: {}, chrome count: {}, # visited: {}, visited_active: {}",
+                                    //    url.to_string(),
+                                    //    count,
+                                    //    chrome_request_count,
+                                    //    visited_peers.len(),
+                                    //    active_peers.len(),
+                                    //);
+                                    visited_peers.insert(url);
+                                }
+                                Err(e) => {
+                                    // Push it back on the stack, try again later.
+                                    if e.is_full() {
+                                        pending_requests.push(peer);
+                                        break;
+                                    } else if e.is_disconnected() {
+                                        return Err(PeerDiscoveryError::MpscSender);
+                                    }
+                                    unreachable!()
+                                }
+                            }
+                        } else {
+                            pending_requests.push(peer);
+                            break;
+                        }
+                    }
+                }
+                match p {
+                    Msg::AddActiveNode(mut url) => {
+                        #[allow(clippy::unwrap_used)]
+                        url.set_port(None).unwrap();
+                        active_peers.insert(url.clone());
+                        visited_peers.insert(url);
+                        count -= 1;
+
+                        chrome_request_count -= 2;
+                        //console_log!(
+                        //    "/peers/all succeeded. count: {}, chrome count: {}, # visited: {}",
+                        //    count,
+                        //    chrome_request_count,
+                        //    visited_peers.len(),
+                        //);
+                    }
+                    Msg::InfoRequestSucceeded(url) => {
+                        chrome_request_count -= 2;
+                        pending_requests.push(NodeRequest::PeersAll(url));
+                        //console_log!(
+                        //    "/info succeeded. count: {}, chrome count: {}, # visited: {}",
+                        //    count,
+                        //    chrome_request_count,
+                        //    visited_peers.len(),
+                        //);
+                    }
+                    Msg::InfoRequestFailedWithoutTimeout(mut url) => {
+                        #[allow(clippy::unwrap_used)]
+                        url.set_port(None).unwrap();
+                        visited_peers.insert(url);
+                        count -= 1;
+
+                        chrome_request_count -= 2;
+                        //console_log!(
+                        //    "/info failed with no timeout. count: {}, chrome count: {}, # visited: {}",
+                        //    count,
+                        //    chrome_request_count,
+                        //    visited_peers.len(),
+                        //);
+                    }
+                    Msg::InfoRequestFailedWithTimeout(mut url) => {
+                        #[allow(clippy::unwrap_used)]
+                        url.set_port(None).unwrap();
+                        visited_peers.insert(url);
+                        count -= 1;
+
+                        chrome_request_count -= 1;
+                        //console_log!(
+                        //    "/info failed WITH timeout. node count: {}, chrome count: {}, # visited: {}",
+                        //    count,
+                        //    chrome_request_count,
+                        //    visited_peers.len(),
+                        //);
+                    }
+                    Msg::PeersAllRequestFailedWithoutTimeout(mut url) => {
+                        #[allow(clippy::unwrap_used)]
+                        url.set_port(None).unwrap();
+                        visited_peers.insert(url);
+                        count -= 1;
+
+                        chrome_request_count -= 2;
+                    }
+                    Msg::PeersAllRequestFailedWithTimeout(mut url) => {
+                        #[allow(clippy::unwrap_used)]
+                        url.set_port(None).unwrap();
+                        visited_peers.insert(url);
+                        count -= 1;
+
+                        chrome_request_count -= 1;
+                    }
+                    Msg::PreflightRequestFailed => {
+                        chrome_request_count -= 1;
+                        //console_log!(
+                        //    "Preflight request failed (by simulation), node count: {}, chrome count: {}, # visited: {}",
+                        //    count,
+                        //    chrome_request_count,
+                        //    visited_peers.len(),
+                        //);
+                    }
+                    Msg::CheckPeers(mut peers) => {
+                        use rand::seq::SliceRandom;
+                        use rand::thread_rng;
+                        peers.shuffle(&mut thread_rng());
+                        if add_peers {
+                            pending_requests.extend(
+                                peers
+                                    .into_iter()
+                                    .map(|p| NodeRequest::Info(p.addr.as_http_url())),
+                            );
+                        } else {
+                            pending_requests_after_timeout.extend(
+                                peers
+                                    .into_iter()
+                                    .map(|p| NodeRequest::Info(p.addr.as_http_url())),
+                            );
+                        }
+                    }
+                }
+                if count == 0 && pending_requests.is_empty() {
+                    break 'loop_;
+                }
+            }
+            C::RxTimeoutSignal => {
+                add_peers = false;
+                while let Some(req) = pending_requests.pop() {
+                    pending_requests_after_timeout.push(req);
+                }
+                //console_log!(
+                //    "GLOBAL TIMEOUT, {} incomplete requests-------------------------",
+                //    pending_requests_after_timeout.len()
+                //);
+            }
+        }
+    }
+
+    drop(tx_node_request);
+    let active_peers: Vec<_> = active_peers
+        .difference(&seeds_set)
+        .into_iter()
+        .cloned()
+        .collect();
+    // Uncomment for debugging
+    //console_log!("Active_peers: {:?}", active_peers);
+    //console_log!(
+    //    "Total # nodes visited: {}, # peers found: {}, # incomplete requests: {}",
+    //    visited_peers.len(),
+    //    active_peers.len(),
+    //    pending_requests_after_timeout.len(),
+    //);
+    //console_log!("Waiting 80sec for Chrome to relinquish pending HTTP requests");
+    crate::wasm_timer::Delay::new(Duration::from_secs(80)).await?;
+    Ok(ChromePeerDiscoveryScan {
+        active_peers,
+        visited_peers,
+        pending_requests: pending_requests_after_timeout,
+        seeds_set,
+    })
+}
+
+/// 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_chrome(
+    tx_msg: futures::channel::mpsc::Sender<Msg>,
+    node_request_stream: impl futures::Stream<Item = NodeRequest> + Send + 'static,
+    max_parallel_tasks: BoundedU16<1, { u16::MAX }>,
+    request_timeout_duration: Duration,
+) {
+    use futures::{SinkExt, StreamExt};
+    use wasm_bindgen_futures::spawn_local;
+
+    let mapped_stream = node_request_stream
+        .map(move |node_request| {
+            let mut tx_msg = tx_msg.clone();
+            async move {
+                spawn_local(async move {
+                    let mut url = node_request.get_url().clone();
+                    #[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),
+                    };
+
+                    // When a timeout is initiated, the message below is how
+                    // Chrome represents the timeout error.
+                    let chrome_timeout_str = "error sending request: JsValue(AbortError: \
+                                             The user aborted a request.";
+                    match node_request {
+                        NodeRequest::Info(url) => match get_info(node_conf).await {
+                            Ok(_) => {
+                                let _ = tx_msg.send(Msg::InfoRequestSucceeded(url)).await;
+                            }
+                            Err(e) => {
+                                if let NodeError::ReqwestError(r) = e {
+                                    if r.to_string().starts_with(chrome_timeout_str) {
+                                        let _ = tx_msg
+                                            .send(Msg::InfoRequestFailedWithTimeout(url))
+                                            .await;
+                                        // This task simulates the waiting of a preflight request
+                                        // that will timeout from no response.
+                                        spawn_local(async move {
+                                            let _ = crate::wasm_timer::Delay::new(
+                                                Duration::from_secs(80),
+                                            )
+                                            .await;
+                                            let _ = tx_msg.send(Msg::PreflightRequestFailed).await;
+                                        });
+                                    } else {
+                                        #[allow(clippy::unwrap_used)]
+                                        let _ = tx_msg
+                                            .send(Msg::InfoRequestFailedWithoutTimeout(url))
+                                            .await;
+                                    }
+                                } else {
+                                    #[allow(clippy::unwrap_used)]
+                                    let _ = tx_msg
+                                        .send(Msg::InfoRequestFailedWithoutTimeout(url))
+                                        .await;
+                                }
+                            }
+                        },
+                        NodeRequest::PeersAll(url) => {
+                            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_msg.send(Msg::CheckPeers(peers)).await.unwrap();
+                                    tx_msg.send(Msg::AddActiveNode(url.clone())).await.unwrap();
+                                }
+                                Err(e) => {
+                                    if let NodeError::ReqwestError(r) = e {
+                                        if r.to_string().starts_with(chrome_timeout_str) {
+                                            let _ = tx_msg
+                                                .send(Msg::PeersAllRequestFailedWithTimeout(url))
+                                                .await;
+
+                                            // This task simulates the waiting of a preflight
+                                            // request that will timeout from no response.
+                                            spawn_local(async move {
+                                                crate::wasm_timer::Delay::new(Duration::from_secs(
+                                                    80,
+                                                ))
+                                                .await
+                                                .unwrap();
+
+                                                let _ =
+                                                    tx_msg.send(Msg::PreflightRequestFailed).await;
+                                            });
+                                        }
+                                    } else {
+                                        let _ = tx_msg
+                                            .send(Msg::PeersAllRequestFailedWithoutTimeout(url))
+                                            .await;
+                                    }
+                                }
+                            }
+                        }
+                    }
+                });
+            }
+        })
+        .buffer_unordered(max_parallel_tasks.get() as usize); // Allow for parallel requests
+
+    let spawn_fn_new = wasm_bindgen_futures::spawn_local;
+
+    // (*) Run stream to completion.
+    spawn_fn_new(mapped_stream.for_each(|_| async move {}));
+}
+
+/// 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),
+    /// /info returns code 200
+    InfoRequestSucceeded(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. In addition a response to the
+    /// preflight request was given (connection refused).
+    InfoRequestFailedWithoutTimeout(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.
+    InfoRequestFailedWithTimeout(Url),
+    /// /peers/all failed with timeout
+    PeersAllRequestFailedWithTimeout(Url),
+    /// /peers/all failed without timeout
+    PeersAllRequestFailedWithoutTimeout(Url),
+    /// Preflight request failed
+    PreflightRequestFailed,
+    /// A list of peers of an active ergo node, returned from a GET on the /peers/all endpoint.
+    CheckPeers(Vec<PeerInfo>),
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
+// Represents HTTP requests to be made of an ergo node. Note: PeersAll > Info, see
+// https://doc.rust-lang.org/stable/std/cmp/trait.Ord.html#derivable
+pub enum NodeRequest {
+    /// /info endpoint
+    Info(Url),
+    /// /peers/all endpoint
+    PeersAll(Url),
+}
+
+impl NodeRequest {
+    fn get_url(&self) -> &Url {
+        match self {
+            NodeRequest::Info(url) => url,
+            NodeRequest::PeersAll(url) => url,
+        }
+    }
+}
+
+#[derive(Debug, Clone)]
+/// This struct stores the results of potentially-multiple calls to `peer_discovery_chrome`. It
+/// allows the user the ability to break up peer discovery into smaller sub-tasks.
+pub struct ChromePeerDiscoveryScan {
+    /// Lists the peers with an active REST API
+    active_peers: Vec<Url>,
+    /// Lists the peers that have already been visited.
+    visited_peers: HashSet<Url>,
+    /// Lists the seed peers that is hardcoded in the ergo reference node.
+    seeds_set: HashSet<Url>,
+    /// Lists node requests which must be made but have yet to due to timeout of a previous scan.
+    pending_requests: BinaryHeap<NodeRequest>,
+}
+
+impl ChromePeerDiscoveryScan {
+    /// Creates new instance from Vec of seed nodes
+    pub fn new(seeds: NonEmptyVec<Url>) -> Self {
+        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);
+        }
+
+        let mut pending_requests = BinaryHeap::new();
+        // Start with requests to seed nodes.
+        for url in &seeds_set {
+            pending_requests.push(NodeRequest::Info(url.clone()));
+        }
+        ChromePeerDiscoveryScan {
+            active_peers: vec![],
+            visited_peers: HashSet::new(),
+            seeds_set,
+            pending_requests,
+        }
+    }
+
+    /// Returns vec of active peers
+    pub fn active_peers(&self) -> Vec<Url> {
+        self.active_peers.clone()
+    }
+}
diff --git a/ergo-rest/src/api/peer_discovery_internals/non_chrome.rs b/ergo-rest/src/api/peer_discovery_internals/non_chrome.rs
new file mode 100644
index 000000000..0814be957
--- /dev/null
+++ b/ergo-rest/src/api/peer_discovery_internals/non_chrome.rs
@@ -0,0 +1,442 @@
+//! 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 super::PeerDiscoverySettings;
+use crate::api::peer_discovery_internals::get_peers_all;
+use crate::error::PeerDiscoveryError;
+use crate::{api::node::get_info, NodeConf, 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;
+
+// Uncomment the following to enable logging on WASM through the `console_log` macro. Taken from
+// https://rustwasm.github.io/wasm-bindgen/examples/console-log.html#srclibrs
+//#[cfg(target_arch = "wasm32")]
+//use wasm_bindgen::prelude::*;
+//
+//
+//#[cfg(target_arch = "wasm32")]
+//#[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);
+//}
+//
+//#[cfg(target_arch = "wasm32")]
+//macro_rules! console_log {
+//// Note that this is using the `log` function imported above during
+//// `bare_bones`
+//($($t:tt)*) => (log(&format_args!($($t)*).to_string()))
+//}
+
+pub(crate) async fn peer_discovery_inner(
+    seeds: NonEmptyVec<Url>,
+    max_parallel_tasks: BoundedU16<1, { u16::MAX }>,
+    timeout: Duration,
+) -> Result<Vec<Url>, PeerDiscoveryError> {
+    let settings = PeerDiscoverySettings {
+        max_parallel_tasks,
+        task_2_buffer_length: max_parallel_tasks.get() as usize,
+        global_timeout: timeout,
+        timeout_of_individual_node_request: Duration::from_secs(4),
+    };
+    #[cfg(not(target_arch = "wasm32"))]
+    let (tx_msg, rx_msg) = tokio::sync::mpsc::channel::<Msg>(settings.task_2_buffer_length);
+    #[cfg(not(target_arch = "wasm32"))]
+    let (tx_url, rx_url) = tokio::sync::mpsc::channel::<Url>(settings.task_2_buffer_length);
+    #[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>(settings.task_2_buffer_length);
+    #[cfg(target_arch = "wasm32")]
+    let (tx_url, rx_url) = futures::channel::mpsc::channel::<Url>(settings.task_2_buffer_length);
+    #[cfg(target_arch = "wasm32")]
+    let url_stream = rx_url;
+    #[cfg(target_arch = "wasm32")]
+    let msg_stream = rx_msg;
+
+    peer_discovery_impl(seeds, tx_msg, msg_stream, tx_url, url_stream, settings).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>,
+    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,
+    settings: PeerDiscoverySettings,
+) -> 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,
+        settings.max_parallel_tasks,
+        settings.timeout_of_individual_node_request,
+    );
+
+    // 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 `settings.global_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 _ = crate::wasm_timer::Delay::new(settings.global_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(settings.global_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);
+
+    // This variable equals to true as long as we're checking for new peer nodes. It is set to false
+    // once the global timeout is reached.
+    let mut add_peers = true;
+
+    '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();
+                        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(mut peers) => {
+                        use rand::seq::SliceRandom;
+                        use rand::thread_rng;
+                        peers.shuffle(&mut thread_rng());
+                        if add_peers {
+                            peer_stack.extend(peers);
+                        }
+                    }
+                }
+            }
+            C::RxTimeoutSignal => {
+                add_peers = false;
+                peer_stack.clear();
+            }
+        }
+    }
+
+    drop(tx_url);
+    let coll: Vec<_> = visited_active_peers
+        .difference(&seeds_set)
+        .into_iter()
+        .cloned()
+        .collect();
+
+    // Uncomment for debugging
+
+    //#[cfg(not(target_arch = "wasm32"))]
+    //println!(
+    //    "Total # nodes visited: {}, # peers found: {}",
+    //    visited_peers.len(),
+    //    coll.len()
+    //);
+    //
+    //#[cfg(target_arch = "wasm32")]
+    //console_log!(
+    //    "Total # nodes visited: {}, # peers found: {}",
+    //    visited_peers.len(),
+    //    coll.len()
+    //);
+    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.
+                    match get_info(node_conf).await {
+                        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;
+                                }
+                            }
+                        }
+                        Err(_) => {
+                            #[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 {}));
+}
+
+/// 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 50313a330..0ae605d6f 100644
--- a/ergo-rest/src/error.rs
+++ b/ergo-rest/src/error.rs
@@ -1,4 +1,5 @@
 use crate::reqwest;
+use derive_more::From;
 use thiserror::Error;
 
 /// Possible errors during the communication with node
@@ -12,7 +13,7 @@ pub enum NodeError {
     InvalidNumericalUrlSegment,
 }
 
-#[derive(Debug, Error)]
+#[derive(Debug, Error, From)]
 /// Peer discovery error
 pub enum PeerDiscoveryError {
     /// `Url` error
@@ -27,4 +28,13 @@ pub enum PeerDiscoveryError {
     /// task spawn error
     #[error("Task spawn error")]
     TaskSpawn,
+    /// IO error
+    #[error("IO error")]
+    IO(std::io::Error),
+    /// Timeout duration is too short
+    #[error("Timeout duration is too short")]
+    TimeoutTooShort,
+    /// There aren't any node requests to be made
+    #[error("There aren't any node requests to be made")]
+    NoPendingNodeRequests,
 }
diff --git a/ergo-rest/src/reqwest/wasm/client.rs b/ergo-rest/src/reqwest/wasm/client.rs
index 017f34f65..a5a8f55b4 100644
--- a/ergo-rest/src/reqwest/wasm/client.rs
+++ b/ergo-rest/src/reqwest/wasm/client.rs
@@ -233,14 +233,14 @@ async fn fetch(req: Request) -> crate::reqwest::Result<Response> {
         }
     }
 
-    if let Some(duration) = req.timeout() {
+    let timeout_handle = 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
+        let handle = window
             .set_timeout_with_callback_and_timeout_and_arguments_0(
                 abort_request_cb.as_ref().unchecked_ref(),
                 duration.as_millis() as i32,
@@ -248,7 +248,10 @@ async fn fetch(req: Request) -> crate::reqwest::Result<Response> {
             .expect("timeout was set");
 
         abort_request_cb.forget();
-    }
+        Some(handle)
+    } else {
+        None
+    };
 
     let js_req = web_sys::Request::new_with_str_and_init(req.url().as_str(), &init)
         .map_err(crate::reqwest::error::wasm)
@@ -259,6 +262,10 @@ async fn fetch(req: Request) -> crate::reqwest::Result<Response> {
     let js_resp = super::promise::<web_sys::Response>(p)
         .await
         .map_err(crate::reqwest::error::request)?;
+    
+    if let Some(handle) = timeout_handle {
+        window.clear_timeout_with_handle(handle);
+    }
 
     // Convert from the js Response
     let mut resp = http::Response::builder().status(js_resp.status());


Test patch:
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
index 5004b5e14..3e2ec7f06 100644
--- a/bindings/ergo-lib-wasm/tests_browser/integration_tests_rest_api.js
+++ b/bindings/ergo-lib-wasm/tests_browser/integration_tests_rest_api.js
@@ -10,7 +10,7 @@ beforeEach(async () => {
 });
 
 it('node REST API get_info endpoint', async () => {
-    let node_conf = new ergo_wasm.NodeConf("127.0.0.1:9053");
+    let node_conf = new ergo_wasm.NodeConf(new URL("http://127.0.0.1:9053"));
     assert(node_conf != null);
     let res = await ergo_wasm.get_info(node_conf);
     assert(res != null);
diff --git a/bindings/ergo-lib-wasm/tests_browser/test_rest_api.js b/bindings/ergo-lib-wasm/tests_browser/test_rest_api.js
index 2e5e45508..ddbbc5081 100644
--- a/bindings/ergo-lib-wasm/tests_browser/test_rest_api.js
+++ b/bindings/ergo-lib-wasm/tests_browser/test_rest_api.js
@@ -10,8 +10,35 @@ beforeEach(async () => {
 // 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: peer_discovery endpoint', async () => {
+    const seeds = get_ergo_node_seeds();
+    // Limit to 150 simultaneous HTTP requests and search for peers for 140 seconds (remember
+    // there's an unavoidable waiting time of 80 seconds, to give Chrome time to relinquish failed
+    // preflight requests)
+    let is_chrome = true;
+    let active_peers = await ergo_wasm.peer_discovery(seeds, 150, 140, is_chrome);
+    assert(active_peers.len() > 0);
+    console.log("Number active peers:", active_peers.len(), ". First active peer: ", active_peers.get(0).href);
+});
+
+it('node REST API: peer_discovery endpoint (INCREMENTAL VERSION)', async () => {
+    const seeds = get_ergo_node_seeds();
+    let scan = new ergo_wasm.ChromePeerDiscoveryScan(seeds);
+
+    scan = await ergo_wasm.incremental_peer_discovery_chrome(scan, 150, 90);
+    let scan_1_len = scan.active_peers().len();
+    console.log("# active peers from first scan:", scan_1_len);
+    scan = await ergo_wasm.incremental_peer_discovery_chrome(scan, 150, 480);
+    let scan_2_len = scan.active_peers().len();
+    console.log("# active peers from second scan:", scan_2_len);
+
+    // The following assert should have `<` instead of `<=`. There is an issue with Github CI, see
+    // https://github.com/ergoplatform/sigma-rust/issues/586
+    assert(scan_1_len <= scan_2_len, "Should have found more peers after second scan!");
+});
+
 it('node REST API: get_nipopow_proof_by_header_id endpoint', async () => {
-    let node_conf = new ergo_wasm.NodeConf("213.239.193.208:9053");
+    let node_conf = new ergo_wasm.NodeConf(new URL("http://213.239.193.208:9053")); //active_peers.get(0));
     assert(node_conf != null);
     const header_id = ergo_wasm.BlockId.from_str("4caa17e62fe66ba7bd69597afdc996ae35b1ff12e0ba90c22ff288a4de10e91b");
     let res = await ergo_wasm.get_nipopow_proof_by_header_id(node_conf, 3, 4, header_id);
@@ -27,8 +54,8 @@ it('node REST API: example SPV workflow', async () => {
 
     // Get NiPoPow proofs from 2 separate ergo nodes
     let proofs = await Promise.all([
-        get_nipopow_proof("147.135.70.51:9053", header_id),
-        get_nipopow_proof("49.205.198.127:9053", header_id),
+        get_nipopow_proof(new URL("http://159.65.11.55:9053"), header_id),
+        get_nipopow_proof(new URL("http://213.239.193.208:9053"), header_id),
     ]);
 
     const genesis_block_id = ergo_wasm.BlockId.from_str("b0244dfc267baca974a4caee06120321562784303a8a688976ae56170e4d175b");
@@ -41,16 +68,16 @@ it('node REST API: example SPV workflow', async () => {
     assert(best_proof.suffix_head().id().equals(header_id), "equality");
 
     // Verify with a 3rd node
-    let node_conf = new ergo_wasm.NodeConf("15.235.145.2:9053");
+    let node_conf = new ergo_wasm.NodeConf(new URL("http://159.65.11.55:9053"));
     let header = await ergo_wasm.get_header(node_conf, header_id);
     assert(header != null, "header should be non-null");
     let merkle_proof = await ergo_wasm.get_blocks_header_id_proof_for_tx_id(node_conf, header_id, tx_id);
-    assert(merkle_proof != null);
-    assert(merkle_proof.valid(header.transactions_root()));
+    assert(merkle_proof != null, "merkle_proof should be non-null");
+    assert(merkle_proof.valid(header.transactions_root()), "merkle_proof should be valid");
 });
 
-async function get_nipopow_proof(node_addr, header_id) {
-    let node_conf = new ergo_wasm.NodeConf(node_addr);
+async function get_nipopow_proof(url, header_id) {
+    let node_conf = new ergo_wasm.NodeConf(url);
     assert(node_conf != null);
 
     // Make sure we're communicating with a node with version >= 4.0.28, otherwise we won't be able
@@ -63,8 +90,8 @@ async function get_nipopow_proof(node_addr, header_id) {
     return proof;
 }
 
-it('node REST API: peer_discovery endpoint', async () => {
-    const seeds = [
+function get_ergo_node_seeds() {
+    return [
         "http://213.239.193.208:9030",
         "http://159.65.11.55:9030",
         "http://165.227.26.175:9030",
@@ -80,6 +107,4 @@ it('node REST API: peer_discovery endpoint', async () => {
         "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!");
-});
+}
\ No newline at end of file