feat: windows ipc provider (named pipe) (#1976)
* fmt: imports * fix: ipc tests * fmt * chore: move ws macros * chore: gate ipc to unix family * chore: make tokio optional * feat: initial named_pipe * feat: windows ipc * chore: update Provider * chore: clippy * chore: use Path instead of OsStr * chore: clippy * fix: docs * lf * lf * test: better subscription tests * docs * fix: ipc doctest * chore: make winapi optional * fix: optional tokio
This commit is contained in:
parent
04e3021dc0
commit
4fd742f8ce
|
@ -1533,6 +1533,7 @@ dependencies = [
|
||||||
"wasm-bindgen-futures",
|
"wasm-bindgen-futures",
|
||||||
"wasm-timer",
|
"wasm-timer",
|
||||||
"web-sys",
|
"web-sys",
|
||||||
|
"winapi",
|
||||||
"ws_stream_wasm",
|
"ws_stream_wasm",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -3702,9 +3703,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "scoped-tls"
|
name = "scoped-tls"
|
||||||
version = "1.0.0"
|
version = "1.0.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2"
|
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "scopeguard"
|
name = "scopeguard"
|
||||||
|
|
|
@ -46,9 +46,12 @@ bytes = { version = "1.3.0", default-features = false, optional = true }
|
||||||
once_cell = "1.17.0"
|
once_cell = "1.17.0"
|
||||||
hashers = "1.0.1"
|
hashers = "1.0.1"
|
||||||
|
|
||||||
|
[target.'cfg(target_family = "windows")'.dependencies]
|
||||||
|
winapi = { version = "0.3", optional = true }
|
||||||
|
|
||||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||||
# tokio
|
# tokio
|
||||||
tokio = { version = "1.18", features = ["time"] }
|
tokio = { version = "1.18", default-features = false, features = ["time"] }
|
||||||
tokio-tungstenite = { version = "0.18.0", default-features = false, features = [
|
tokio-tungstenite = { version = "0.18.0", default-features = false, features = [
|
||||||
"connect",
|
"connect",
|
||||||
], optional = true }
|
], optional = true }
|
||||||
|
@ -71,8 +74,9 @@ tempfile = "3.3.0"
|
||||||
[features]
|
[features]
|
||||||
default = ["ws", "rustls"]
|
default = ["ws", "rustls"]
|
||||||
celo = ["ethers-core/celo"]
|
celo = ["ethers-core/celo"]
|
||||||
|
|
||||||
ws = ["tokio-tungstenite", "futures-channel"]
|
ws = ["tokio-tungstenite", "futures-channel"]
|
||||||
ipc = ["tokio/io-util", "bytes", "futures-channel"]
|
ipc = ["tokio/io-util", "bytes", "futures-channel", "winapi"]
|
||||||
|
|
||||||
openssl = ["tokio-tungstenite/native-tls", "reqwest/native-tls"]
|
openssl = ["tokio-tungstenite/native-tls", "reqwest/native-tls"]
|
||||||
# we use the webpki roots so we can build static binaries w/o any root cert dependencies
|
# we use the webpki roots so we can build static binaries w/o any root cert dependencies
|
||||||
|
|
|
@ -3,8 +3,8 @@
|
||||||
#![deny(rustdoc::broken_intra_doc_links)]
|
#![deny(rustdoc::broken_intra_doc_links)]
|
||||||
#![allow(clippy::type_complexity)]
|
#![allow(clippy::type_complexity)]
|
||||||
#![doc = include_str!("../README.md")]
|
#![doc = include_str!("../README.md")]
|
||||||
|
|
||||||
mod transports;
|
mod transports;
|
||||||
use futures_util::future::join_all;
|
|
||||||
pub use transports::*;
|
pub use transports::*;
|
||||||
|
|
||||||
mod provider;
|
mod provider;
|
||||||
|
@ -40,7 +40,11 @@ pub mod erc;
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use auto_impl::auto_impl;
|
use auto_impl::auto_impl;
|
||||||
use ethers_core::types::transaction::{eip2718::TypedTransaction, eip2930::AccessListWithGasUsed};
|
use ethers_core::types::{
|
||||||
|
transaction::{eip2718::TypedTransaction, eip2930::AccessListWithGasUsed},
|
||||||
|
*,
|
||||||
|
};
|
||||||
|
use futures_util::future::join_all;
|
||||||
use serde::{de::DeserializeOwned, Serialize};
|
use serde::{de::DeserializeOwned, Serialize};
|
||||||
use std::{error::Error, fmt::Debug, future::Future, pin::Pin};
|
use std::{error::Error, fmt::Debug, future::Future, pin::Pin};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
@ -75,7 +79,6 @@ pub trait JsonRpcClient: Debug + Send + Sync {
|
||||||
R: DeserializeOwned;
|
R: DeserializeOwned;
|
||||||
}
|
}
|
||||||
|
|
||||||
use ethers_core::types::*;
|
|
||||||
pub trait FromErr<T> {
|
pub trait FromErr<T> {
|
||||||
fn from(src: T) -> Self;
|
fn from(src: T) -> Self;
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,26 +21,24 @@ use ethers_core::{
|
||||||
abi::{self, Detokenize, ParamType},
|
abi::{self, Detokenize, ParamType},
|
||||||
types::{
|
types::{
|
||||||
transaction::{eip2718::TypedTransaction, eip2930::AccessListWithGasUsed},
|
transaction::{eip2718::TypedTransaction, eip2930::AccessListWithGasUsed},
|
||||||
Address, Block, BlockId, BlockNumber, BlockTrace, Bytes, EIP1186ProofResponse, FeeHistory,
|
Address, Block, BlockId, BlockNumber, BlockTrace, Bytes, Chain, EIP1186ProofResponse,
|
||||||
Filter, FilterBlockOption, GethDebugTracingCallOptions, GethDebugTracingOptions, GethTrace,
|
FeeHistory, Filter, FilterBlockOption, GethDebugTracingCallOptions,
|
||||||
Log, NameOrAddress, Selector, Signature, Trace, TraceFilter, TraceType, Transaction,
|
GethDebugTracingOptions, GethTrace, Log, NameOrAddress, Selector, Signature, Trace,
|
||||||
TransactionReceipt, TransactionRequest, TxHash, TxpoolContent, TxpoolInspect, TxpoolStatus,
|
TraceFilter, TraceType, Transaction, TransactionReceipt, TransactionRequest, TxHash,
|
||||||
H256, U256, U64,
|
TxpoolContent, TxpoolInspect, TxpoolStatus, H256, U256, U64,
|
||||||
},
|
},
|
||||||
utils,
|
utils,
|
||||||
};
|
};
|
||||||
|
use futures_util::{lock::Mutex, try_join};
|
||||||
use hex::FromHex;
|
use hex::FromHex;
|
||||||
use serde::{de::DeserializeOwned, Serialize};
|
use serde::{de::DeserializeOwned, Serialize};
|
||||||
use thiserror::Error;
|
|
||||||
use url::{ParseError, Url};
|
|
||||||
|
|
||||||
use ethers_core::types::Chain;
|
|
||||||
use futures_util::{lock::Mutex, try_join};
|
|
||||||
use std::{
|
use std::{
|
||||||
collections::VecDeque, convert::TryFrom, fmt::Debug, str::FromStr, sync::Arc, time::Duration,
|
collections::VecDeque, convert::TryFrom, fmt::Debug, str::FromStr, sync::Arc, time::Duration,
|
||||||
};
|
};
|
||||||
|
use thiserror::Error;
|
||||||
use tracing::trace;
|
use tracing::trace;
|
||||||
use tracing_futures::Instrument;
|
use tracing_futures::Instrument;
|
||||||
|
use url::{ParseError, Url};
|
||||||
|
|
||||||
#[derive(Copy, Clone)]
|
#[derive(Copy, Clone)]
|
||||||
pub enum NodeClient {
|
pub enum NodeClient {
|
||||||
|
@ -1415,9 +1413,14 @@ impl Provider<crate::Ws> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(all(target_family = "unix", feature = "ipc"))]
|
#[cfg(all(feature = "ipc", any(unix, windows)))]
|
||||||
impl Provider<crate::Ipc> {
|
impl Provider<crate::Ipc> {
|
||||||
/// Direct connection to an IPC socket.
|
#[cfg_attr(unix, doc = "Connects to the Unix socket at the provided path.")]
|
||||||
|
#[cfg_attr(windows, doc = "Connects to the named pipe at the provided path.\n")]
|
||||||
|
#[cfg_attr(
|
||||||
|
windows,
|
||||||
|
doc = r"Note: the path must be the fully qualified, like: `\\.\pipe\<name>`."
|
||||||
|
)]
|
||||||
pub async fn connect_ipc(path: impl AsRef<std::path::Path>) -> Result<Self, ProviderError> {
|
pub async fn connect_ipc(path: impl AsRef<std::path::Path>) -> Result<Self, ProviderError> {
|
||||||
let ipc = crate::Ipc::connect(path).await?;
|
let ipc = crate::Ipc::connect(path).await?;
|
||||||
Ok(Self::new(ipc))
|
Ok(Self::new(ipc))
|
||||||
|
|
|
@ -1,17 +1,16 @@
|
||||||
// Code adapted from: https://github.com/althea-net/guac_rs/tree/master/web3/src/jsonrpc
|
// Code adapted from: https://github.com/althea-net/guac_rs/tree/master/web3/src/jsonrpc
|
||||||
use std::fmt;
|
|
||||||
|
|
||||||
|
use ethers_core::types::U256;
|
||||||
use serde::{
|
use serde::{
|
||||||
de::{self, MapAccess, Unexpected, Visitor},
|
de::{self, MapAccess, Unexpected, Visitor},
|
||||||
Deserialize, Serialize,
|
Deserialize, Serialize,
|
||||||
};
|
};
|
||||||
use serde_json::{value::RawValue, Value};
|
use serde_json::{value::RawValue, Value};
|
||||||
|
use std::fmt;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use ethers_core::types::U256;
|
|
||||||
|
|
||||||
#[derive(Deserialize, Debug, Clone, Error)]
|
|
||||||
/// A JSON-RPC 2.0 error
|
/// A JSON-RPC 2.0 error
|
||||||
|
#[derive(Deserialize, Debug, Clone, Error)]
|
||||||
pub struct JsonRpcError {
|
pub struct JsonRpcError {
|
||||||
/// The error code
|
/// The error code
|
||||||
pub code: i64,
|
pub code: i64,
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
// Code adapted from: https://github.com/althea-net/guac_rs/tree/master/web3/src/jsonrpc
|
// Code adapted from: https://github.com/althea-net/guac_rs/tree/master/web3/src/jsonrpc
|
||||||
use crate::{provider::ProviderError, JsonRpcClient};
|
|
||||||
|
|
||||||
|
use super::common::{Authorization, JsonRpcError, Request, Response};
|
||||||
|
use crate::{provider::ProviderError, JsonRpcClient};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use reqwest::{header::HeaderValue, Client, Error as ReqwestError};
|
use reqwest::{header::HeaderValue, Client, Error as ReqwestError};
|
||||||
use serde::{de::DeserializeOwned, Serialize};
|
use serde::{de::DeserializeOwned, Serialize};
|
||||||
|
@ -11,8 +12,6 @@ use std::{
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use super::common::{Authorization, JsonRpcError, Request, Response};
|
|
||||||
|
|
||||||
/// A low-level JSON-RPC Client over HTTP.
|
/// A low-level JSON-RPC Client over HTTP.
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
|
|
|
@ -1,7 +1,22 @@
|
||||||
|
use super::common::Params;
|
||||||
|
use crate::{
|
||||||
|
provider::ProviderError,
|
||||||
|
transports::common::{JsonRpcError, Request, Response},
|
||||||
|
JsonRpcClient, PubsubClient,
|
||||||
|
};
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use bytes::{Buf, BytesMut};
|
||||||
|
use ethers_core::types::U256;
|
||||||
|
use futures_channel::mpsc;
|
||||||
|
use futures_util::stream::StreamExt;
|
||||||
|
use hashers::fx_hash::FxHasher64;
|
||||||
|
use serde::{de::DeserializeOwned, Serialize};
|
||||||
|
use serde_json::{value::RawValue, Deserializer};
|
||||||
use std::{
|
use std::{
|
||||||
cell::RefCell,
|
cell::RefCell,
|
||||||
convert::Infallible,
|
convert::Infallible,
|
||||||
hash::BuildHasherDefault,
|
hash::BuildHasherDefault,
|
||||||
|
io,
|
||||||
path::Path,
|
path::Path,
|
||||||
sync::{
|
sync::{
|
||||||
atomic::{AtomicU64, Ordering},
|
atomic::{AtomicU64, Ordering},
|
||||||
|
@ -9,40 +24,194 @@ use std::{
|
||||||
},
|
},
|
||||||
thread,
|
thread,
|
||||||
};
|
};
|
||||||
|
|
||||||
use async_trait::async_trait;
|
|
||||||
use bytes::{Buf as _, BytesMut};
|
|
||||||
use ethers_core::types::U256;
|
|
||||||
use futures_channel::mpsc;
|
|
||||||
use futures_util::stream::StreamExt as _;
|
|
||||||
use hashers::fx_hash::FxHasher64;
|
|
||||||
use serde::{de::DeserializeOwned, Serialize};
|
|
||||||
use serde_json::{value::RawValue, Deserializer};
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use tokio::{
|
use tokio::{
|
||||||
io::{AsyncReadExt as _, AsyncWriteExt as _, BufReader},
|
io::{AsyncReadExt, AsyncWriteExt, BufReader},
|
||||||
net::{
|
|
||||||
unix::{ReadHalf, WriteHalf},
|
|
||||||
UnixStream,
|
|
||||||
},
|
|
||||||
runtime,
|
runtime,
|
||||||
sync::oneshot::{self, error::RecvError},
|
sync::oneshot::{self, error::RecvError},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
|
||||||
provider::ProviderError,
|
|
||||||
transports::common::{JsonRpcError, Request, Response},
|
|
||||||
JsonRpcClient, PubsubClient,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::common::Params;
|
|
||||||
|
|
||||||
type FxHashMap<K, V> = std::collections::HashMap<K, V, BuildHasherDefault<FxHasher64>>;
|
type FxHashMap<K, V> = std::collections::HashMap<K, V, BuildHasherDefault<FxHasher64>>;
|
||||||
|
|
||||||
type Pending = oneshot::Sender<Result<Box<RawValue>, JsonRpcError>>;
|
type Pending = oneshot::Sender<Result<Box<RawValue>, JsonRpcError>>;
|
||||||
type Subscription = mpsc::UnboundedSender<Box<RawValue>>;
|
type Subscription = mpsc::UnboundedSender<Box<RawValue>>;
|
||||||
|
|
||||||
/// Unix Domain Sockets (IPC) transport.
|
#[cfg(unix)]
|
||||||
|
#[doc(hidden)]
|
||||||
|
mod imp {
|
||||||
|
pub(super) use tokio::net::{
|
||||||
|
unix::{ReadHalf, WriteHalf},
|
||||||
|
UnixStream as Stream,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
#[doc(hidden)]
|
||||||
|
mod imp {
|
||||||
|
use super::*;
|
||||||
|
use std::{
|
||||||
|
ops::{Deref, DerefMut},
|
||||||
|
pin::Pin,
|
||||||
|
task::{Context, Poll},
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
use tokio::{
|
||||||
|
io::{AsyncRead, AsyncWrite, ReadBuf},
|
||||||
|
net::windows::named_pipe::{ClientOptions, NamedPipeClient},
|
||||||
|
time::sleep,
|
||||||
|
};
|
||||||
|
use winapi::shared::winerror;
|
||||||
|
|
||||||
|
/// Wrapper around [NamedPipeClient] to have the same methods as a UnixStream.
|
||||||
|
///
|
||||||
|
/// Should not be exported.
|
||||||
|
#[repr(transparent)]
|
||||||
|
pub(super) struct Stream(pub NamedPipeClient);
|
||||||
|
|
||||||
|
impl Deref for Stream {
|
||||||
|
type Target = NamedPipeClient;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DerefMut for Stream {
|
||||||
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
|
&mut self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Stream {
|
||||||
|
pub async fn connect(addr: impl AsRef<Path>) -> Result<Self, io::Error> {
|
||||||
|
let addr = addr.as_ref().as_os_str();
|
||||||
|
loop {
|
||||||
|
match ClientOptions::new().open(addr) {
|
||||||
|
Ok(client) => break Ok(Self(client)),
|
||||||
|
Err(e) if e.raw_os_error() == Some(winerror::ERROR_PIPE_BUSY as i32) => (),
|
||||||
|
Err(e) => break Err(e),
|
||||||
|
}
|
||||||
|
|
||||||
|
sleep(Duration::from_millis(50)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unsafe_code)]
|
||||||
|
pub fn split(&mut self) -> (ReadHalf, WriteHalf) {
|
||||||
|
// SAFETY: ReadHalf cannot write but still needs a mutable reference for polling.
|
||||||
|
// NamedPipeClient calls its `io` using immutable references, but it's private.
|
||||||
|
let self1 = unsafe { &mut *(self as *mut Self) };
|
||||||
|
let self2 = self;
|
||||||
|
(ReadHalf(self1), WriteHalf(self2))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsyncRead for Stream {
|
||||||
|
fn poll_read(
|
||||||
|
self: Pin<&mut Self>,
|
||||||
|
cx: &mut Context<'_>,
|
||||||
|
buf: &mut ReadBuf<'_>,
|
||||||
|
) -> Poll<io::Result<()>> {
|
||||||
|
let this = Pin::new(&mut self.get_mut().0);
|
||||||
|
this.poll_read(cx, buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsyncWrite for Stream {
|
||||||
|
fn poll_write(
|
||||||
|
self: Pin<&mut Self>,
|
||||||
|
cx: &mut Context<'_>,
|
||||||
|
buf: &[u8],
|
||||||
|
) -> Poll<io::Result<usize>> {
|
||||||
|
let this = Pin::new(&mut self.get_mut().0);
|
||||||
|
this.poll_write(cx, buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll_write_vectored(
|
||||||
|
self: Pin<&mut Self>,
|
||||||
|
cx: &mut Context<'_>,
|
||||||
|
bufs: &[io::IoSlice<'_>],
|
||||||
|
) -> Poll<io::Result<usize>> {
|
||||||
|
let this = Pin::new(&mut self.get_mut().0);
|
||||||
|
this.poll_write_vectored(cx, bufs)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
|
||||||
|
Poll::Ready(Ok(()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
|
||||||
|
self.poll_flush(cx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) struct ReadHalf<'a>(pub &'a mut Stream);
|
||||||
|
|
||||||
|
pub(super) struct WriteHalf<'a>(pub &'a mut Stream);
|
||||||
|
|
||||||
|
impl AsyncRead for ReadHalf<'_> {
|
||||||
|
fn poll_read(
|
||||||
|
self: Pin<&mut Self>,
|
||||||
|
cx: &mut Context<'_>,
|
||||||
|
buf: &mut ReadBuf<'_>,
|
||||||
|
) -> Poll<io::Result<()>> {
|
||||||
|
let this = Pin::new(&mut self.get_mut().0 .0);
|
||||||
|
this.poll_read(cx, buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsyncWrite for WriteHalf<'_> {
|
||||||
|
fn poll_write(
|
||||||
|
self: Pin<&mut Self>,
|
||||||
|
cx: &mut Context<'_>,
|
||||||
|
buf: &[u8],
|
||||||
|
) -> Poll<io::Result<usize>> {
|
||||||
|
let this = Pin::new(&mut self.get_mut().0 .0);
|
||||||
|
this.poll_write(cx, buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll_write_vectored(
|
||||||
|
self: Pin<&mut Self>,
|
||||||
|
cx: &mut Context<'_>,
|
||||||
|
bufs: &[io::IoSlice<'_>],
|
||||||
|
) -> Poll<io::Result<usize>> {
|
||||||
|
let this = Pin::new(&mut self.get_mut().0 .0);
|
||||||
|
this.poll_write_vectored(cx, bufs)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_write_vectored(&self) -> bool {
|
||||||
|
self.0.is_write_vectored()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
|
||||||
|
let this = Pin::new(&mut self.get_mut().0 .0);
|
||||||
|
this.poll_flush(cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
|
||||||
|
self.poll_flush(cx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
use self::imp::*;
|
||||||
|
|
||||||
|
#[cfg_attr(unix, doc = "A JSON-RPC Client over Unix IPC.")]
|
||||||
|
#[cfg_attr(windows, doc = "A JSON-RPC Client over named pipes.")]
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```no_run
|
||||||
|
/// # async fn foo() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
/// use ethers_providers::Ipc;
|
||||||
|
///
|
||||||
|
/// // the ipc's path
|
||||||
|
#[cfg_attr(unix, doc = r#"let path = "/home/user/.local/share/reth/reth.ipc";"#)]
|
||||||
|
#[cfg_attr(windows, doc = r#"let path = r"\\.\pipe\reth.ipc";"#)]
|
||||||
|
/// let ipc = Ipc::connect(path).await?;
|
||||||
|
/// # Ok(())
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Ipc {
|
pub struct Ipc {
|
||||||
id: Arc<AtomicU64>,
|
id: Arc<AtomicU64>,
|
||||||
|
@ -57,12 +226,17 @@ enum TransportMessage {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Ipc {
|
impl Ipc {
|
||||||
/// Creates a new IPC transport from a given path using Unix sockets.
|
#[cfg_attr(unix, doc = "Connects to the Unix socket at the provided path.")]
|
||||||
|
#[cfg_attr(windows, doc = "Connects to the named pipe at the provided path.\n")]
|
||||||
|
#[cfg_attr(
|
||||||
|
windows,
|
||||||
|
doc = r"Note: the path must be the fully qualified, like: `\\.\pipe\<name>`."
|
||||||
|
)]
|
||||||
pub async fn connect(path: impl AsRef<Path>) -> Result<Self, IpcError> {
|
pub async fn connect(path: impl AsRef<Path>) -> Result<Self, IpcError> {
|
||||||
let id = Arc::new(AtomicU64::new(1));
|
let id = Arc::new(AtomicU64::new(1));
|
||||||
let (request_tx, request_rx) = mpsc::unbounded();
|
let (request_tx, request_rx) = mpsc::unbounded();
|
||||||
|
|
||||||
let stream = UnixStream::connect(path).await?;
|
let stream = Stream::connect(path).await?;
|
||||||
spawn_ipc_server(stream, request_rx);
|
spawn_ipc_server(stream, request_rx);
|
||||||
|
|
||||||
Ok(Self { id, request_tx })
|
Ok(Self { id, request_tx })
|
||||||
|
@ -121,11 +295,11 @@ impl PubsubClient for Ipc {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn spawn_ipc_server(stream: UnixStream, request_rx: mpsc::UnboundedReceiver<TransportMessage>) {
|
fn spawn_ipc_server(stream: Stream, request_rx: mpsc::UnboundedReceiver<TransportMessage>) {
|
||||||
// 65 KiB should be more than enough for this thread, as all unbounded data
|
// 256 Kb should be more than enough for this thread, as all unbounded data
|
||||||
// growth occurs on heap-allocated data structures and buffers and the call
|
// growth occurs on heap-allocated data structures and buffers and the call
|
||||||
// stack is not going to do anything crazy either
|
// stack is not going to do anything crazy either
|
||||||
const STACK_SIZE: usize = 1 << 16;
|
const STACK_SIZE: usize = 1 << 18;
|
||||||
// spawn a light-weight thread with a thread-local async runtime just for
|
// spawn a light-weight thread with a thread-local async runtime just for
|
||||||
// sending and receiving data over the IPC socket
|
// sending and receiving data over the IPC socket
|
||||||
let _ = thread::Builder::new()
|
let _ = thread::Builder::new()
|
||||||
|
@ -142,10 +316,7 @@ fn spawn_ipc_server(stream: UnixStream, request_rx: mpsc::UnboundedReceiver<Tran
|
||||||
.expect("failed to spawn ipc server thread");
|
.expect("failed to spawn ipc server thread");
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn run_ipc_server(
|
async fn run_ipc_server(mut stream: Stream, request_rx: mpsc::UnboundedReceiver<TransportMessage>) {
|
||||||
mut stream: UnixStream,
|
|
||||||
request_rx: mpsc::UnboundedReceiver<TransportMessage>,
|
|
||||||
) {
|
|
||||||
// the shared state for both reads & writes
|
// the shared state for both reads & writes
|
||||||
let shared = Shared {
|
let shared = Shared {
|
||||||
pending: FxHashMap::with_capacity_and_hasher(64, BuildHasherDefault::default()).into(),
|
pending: FxHashMap::with_capacity_and_hasher(64, BuildHasherDefault::default()).into(),
|
||||||
|
@ -289,8 +460,8 @@ impl Shared {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
|
||||||
/// Error thrown when sending or receiving an IPC message.
|
/// Error thrown when sending or receiving an IPC message.
|
||||||
|
#[derive(Debug, Error)]
|
||||||
pub enum IpcError {
|
pub enum IpcError {
|
||||||
/// Thrown if deserialization failed
|
/// Thrown if deserialization failed
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
|
@ -298,7 +469,7 @@ pub enum IpcError {
|
||||||
|
|
||||||
/// std IO error forwarding.
|
/// std IO error forwarding.
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
IoError(#[from] std::io::Error),
|
IoError(#[from] io::Error),
|
||||||
|
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
/// Thrown if the response could not be parsed
|
/// Thrown if the response could not be parsed
|
||||||
|
@ -319,22 +490,30 @@ impl From<IpcError> for ProviderError {
|
||||||
ProviderError::JsonRpcClientError(Box::new(src))
|
ProviderError::JsonRpcClientError(Box::new(src))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#[cfg(all(test, target_family = "unix"))]
|
|
||||||
#[cfg(not(feature = "celo"))]
|
#[cfg(test)]
|
||||||
mod test {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use ethers_core::{
|
use ethers_core::utils::{Geth, GethInstance};
|
||||||
types::{Block, TxHash, U256},
|
|
||||||
utils::Geth,
|
|
||||||
};
|
|
||||||
use tempfile::NamedTempFile;
|
use tempfile::NamedTempFile;
|
||||||
|
|
||||||
|
async fn connect() -> (Ipc, GethInstance) {
|
||||||
|
let temp_file = NamedTempFile::new().unwrap();
|
||||||
|
let path = temp_file.into_temp_path().to_path_buf();
|
||||||
|
let geth = Geth::new().block_time(1u64).ipc_path(&path).spawn();
|
||||||
|
|
||||||
|
// [Windows named pipes](https://learn.microsoft.com/en-us/windows/win32/ipc/named-pipes)
|
||||||
|
// are located at `\\<machine_address>\pipe\<pipe_name>`.
|
||||||
|
#[cfg(windows)]
|
||||||
|
let path = format!(r"\\.\pipe\{}", path.display());
|
||||||
|
let ipc = Ipc::connect(path).await.unwrap();
|
||||||
|
|
||||||
|
(ipc, geth)
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn request() {
|
async fn request() {
|
||||||
let temp_file = NamedTempFile::new().unwrap();
|
let (ipc, _geth) = connect().await;
|
||||||
let path = temp_file.into_temp_path().to_path_buf();
|
|
||||||
let _geth = Geth::new().block_time(1u64).ipc_path(&path).spawn();
|
|
||||||
let ipc = Ipc::connect(path).await.unwrap();
|
|
||||||
|
|
||||||
let block_num: U256 = ipc.request("eth_blockNumber", ()).await.unwrap();
|
let block_num: U256 = ipc.request("eth_blockNumber", ()).await.unwrap();
|
||||||
std::thread::sleep(std::time::Duration::new(3, 0));
|
std::thread::sleep(std::time::Duration::new(3, 0));
|
||||||
|
@ -343,25 +522,25 @@ mod test {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
#[cfg(not(feature = "celo"))]
|
||||||
async fn subscription() {
|
async fn subscription() {
|
||||||
let temp_file = NamedTempFile::new().unwrap();
|
use ethers_core::types::{Block, TxHash};
|
||||||
let path = temp_file.into_temp_path().to_path_buf();
|
|
||||||
let _geth = Geth::new().block_time(2u64).ipc_path(&path).spawn();
|
|
||||||
let ipc = Ipc::connect(path).await.unwrap();
|
|
||||||
|
|
||||||
let sub_id: U256 = ipc.request("eth_subscribe", ["newHeads"]).await.unwrap();
|
let (ipc, _geth) = connect().await;
|
||||||
let mut stream = ipc.subscribe(sub_id).unwrap();
|
|
||||||
|
|
||||||
// Subscribing requires sending the sub request and then subscribing to
|
// Subscribing requires sending the sub request and then subscribing to
|
||||||
// the returned sub_id
|
// the returned sub_id
|
||||||
let block_num: u64 = ipc.request::<_, U256>("eth_blockNumber", ()).await.unwrap().as_u64();
|
let sub_id: U256 = ipc.request("eth_subscribe", ["newHeads"]).await.unwrap();
|
||||||
let mut blocks = Vec::new();
|
let stream = ipc.subscribe(sub_id).unwrap();
|
||||||
for _ in 0..3 {
|
|
||||||
let item = stream.next().await.unwrap();
|
let blocks: Vec<u64> = stream
|
||||||
|
.take(3)
|
||||||
|
.map(|item| {
|
||||||
let block: Block<TxHash> = serde_json::from_str(item.get()).unwrap();
|
let block: Block<TxHash> = serde_json::from_str(item.get()).unwrap();
|
||||||
blocks.push(block.number.unwrap_or_default().as_u64());
|
block.number.unwrap_or_default().as_u64()
|
||||||
}
|
})
|
||||||
let offset = blocks[0] - block_num;
|
.collect()
|
||||||
assert_eq!(blocks, &[block_num + offset, block_num + offset + 1, block_num + offset + 2])
|
.await;
|
||||||
|
assert_eq!(blocks, vec![1, 2, 3]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
use crate::{JsonRpcClient, ProviderError};
|
use crate::{JsonRpcClient, ProviderError};
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use serde::{de::DeserializeOwned, Serialize};
|
use serde::{de::DeserializeOwned, Serialize};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
|
@ -1,32 +1,14 @@
|
||||||
mod common;
|
mod common;
|
||||||
pub use common::Authorization;
|
pub use common::Authorization;
|
||||||
|
|
||||||
// only used with WS
|
|
||||||
#[cfg(feature = "ws")]
|
|
||||||
macro_rules! if_wasm {
|
|
||||||
($($item:item)*) => {$(
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
|
||||||
$item
|
|
||||||
)*}
|
|
||||||
}
|
|
||||||
|
|
||||||
// only used with WS
|
|
||||||
#[cfg(feature = "ws")]
|
|
||||||
macro_rules! if_not_wasm {
|
|
||||||
($($item:item)*) => {$(
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
$item
|
|
||||||
)*}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(all(target_family = "unix", feature = "ipc"))]
|
|
||||||
mod ipc;
|
|
||||||
#[cfg(all(target_family = "unix", feature = "ipc"))]
|
|
||||||
pub use ipc::{Ipc, IpcError};
|
|
||||||
|
|
||||||
mod http;
|
mod http;
|
||||||
pub use self::http::{ClientError as HttpClientError, Provider as Http};
|
pub use self::http::{ClientError as HttpClientError, Provider as Http};
|
||||||
|
|
||||||
|
#[cfg(all(feature = "ipc", any(unix, windows)))]
|
||||||
|
mod ipc;
|
||||||
|
#[cfg(all(feature = "ipc", any(unix, windows)))]
|
||||||
|
pub use ipc::{Ipc, IpcError};
|
||||||
|
|
||||||
#[cfg(feature = "ws")]
|
#[cfg(feature = "ws")]
|
||||||
mod ws;
|
mod ws;
|
||||||
#[cfg(feature = "ws")]
|
#[cfg(feature = "ws")]
|
||||||
|
|
|
@ -1,10 +1,3 @@
|
||||||
use std::{
|
|
||||||
fmt::Debug,
|
|
||||||
future::Future,
|
|
||||||
pin::Pin,
|
|
||||||
task::{Context, Poll},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{provider::ProviderError, JsonRpcClient, PubsubClient};
|
use crate::{provider::ProviderError, JsonRpcClient, PubsubClient};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use ethers_core::types::{U256, U64};
|
use ethers_core::types::{U256, U64};
|
||||||
|
@ -12,6 +5,12 @@ use futures_core::Stream;
|
||||||
use futures_util::{future::join_all, FutureExt, StreamExt};
|
use futures_util::{future::join_all, FutureExt, StreamExt};
|
||||||
use serde::{de::DeserializeOwned, Serialize};
|
use serde::{de::DeserializeOwned, Serialize};
|
||||||
use serde_json::{value::RawValue, Value};
|
use serde_json::{value::RawValue, Value};
|
||||||
|
use std::{
|
||||||
|
fmt::Debug,
|
||||||
|
future::Future,
|
||||||
|
pin::Pin,
|
||||||
|
task::{Context, Poll},
|
||||||
|
};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
/// A provider that bundles multiple providers and only returns a value to the
|
/// A provider that bundles multiple providers and only returns a value to the
|
||||||
|
|
|
@ -2,11 +2,8 @@
|
||||||
//! and uses a dedicated client for read and the other for write operations
|
//! and uses a dedicated client for read and the other for write operations
|
||||||
|
|
||||||
use crate::{provider::ProviderError, JsonRpcClient};
|
use crate::{provider::ProviderError, JsonRpcClient};
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
|
||||||
use serde::{de::DeserializeOwned, Serialize};
|
use serde::{de::DeserializeOwned, Serialize};
|
||||||
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
/// A client containing two clients.
|
/// A client containing two clients.
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
|
use super::common::{Params, Response};
|
||||||
use crate::{
|
use crate::{
|
||||||
provider::ProviderError,
|
provider::ProviderError,
|
||||||
transports::common::{JsonRpcError, Request},
|
transports::common::{JsonRpcError, Request},
|
||||||
JsonRpcClient, PubsubClient,
|
JsonRpcClient, PubsubClient,
|
||||||
};
|
};
|
||||||
use ethers_core::types::U256;
|
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
use ethers_core::types::U256;
|
||||||
use futures_channel::{mpsc, oneshot};
|
use futures_channel::{mpsc, oneshot};
|
||||||
use futures_util::{
|
use futures_util::{
|
||||||
sink::{Sink, SinkExt},
|
sink::{Sink, SinkExt},
|
||||||
|
@ -24,7 +24,19 @@ use std::{
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use tracing::trace;
|
use tracing::trace;
|
||||||
|
|
||||||
use super::common::{Params, Response};
|
macro_rules! if_wasm {
|
||||||
|
($($item:item)*) => {$(
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
$item
|
||||||
|
)*}
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! if_not_wasm {
|
||||||
|
($($item:item)*) => {$(
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
$item
|
||||||
|
)*}
|
||||||
|
}
|
||||||
|
|
||||||
if_wasm! {
|
if_wasm! {
|
||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
|
@ -84,11 +96,13 @@ enum Instruction {
|
||||||
|
|
||||||
/// A JSON-RPC Client over Websockets.
|
/// A JSON-RPC Client over Websockets.
|
||||||
///
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
/// ```no_run
|
/// ```no_run
|
||||||
/// # async fn foo() -> Result<(), Box<dyn std::error::Error>> {
|
/// # async fn foo() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
/// use ethers_providers::Ws;
|
/// use ethers_providers::Ws;
|
||||||
///
|
///
|
||||||
/// let ws = Ws::connect("wss://localhost:8545").await?;
|
/// let ws = Ws::connect("ws://localhost:8545").await?;
|
||||||
/// # Ok(())
|
/// # Ok(())
|
||||||
/// # }
|
/// # }
|
||||||
/// ```
|
/// ```
|
||||||
|
@ -427,8 +441,8 @@ fn to_client_error<T: Debug>(err: T) -> ClientError {
|
||||||
ClientError::ChannelError(format!("{err:?}"))
|
ClientError::ChannelError(format!("{err:?}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
|
||||||
/// Error thrown when sending a WS message
|
/// Error thrown when sending a WS message
|
||||||
|
#[derive(Debug, Error)]
|
||||||
pub enum ClientError {
|
pub enum ClientError {
|
||||||
/// Thrown if deserialization failed
|
/// Thrown if deserialization failed
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
|
@ -488,15 +502,10 @@ impl From<ClientError> for ProviderError {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(all(test, not(target_arch = "wasm32")))]
|
||||||
#[cfg(not(feature = "celo"))]
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use ethers_core::{
|
use ethers_core::{types::U256, utils::Anvil};
|
||||||
types::{Block, TxHash, U256},
|
|
||||||
utils::Anvil,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn request() {
|
async fn request() {
|
||||||
|
@ -504,29 +513,33 @@ mod tests {
|
||||||
let ws = Ws::connect(anvil.ws_endpoint()).await.unwrap();
|
let ws = Ws::connect(anvil.ws_endpoint()).await.unwrap();
|
||||||
|
|
||||||
let block_num: U256 = ws.request("eth_blockNumber", ()).await.unwrap();
|
let block_num: U256 = ws.request("eth_blockNumber", ()).await.unwrap();
|
||||||
std::thread::sleep(std::time::Duration::new(3, 0));
|
tokio::time::sleep(std::time::Duration::from_secs(3)).await;
|
||||||
let block_num2: U256 = ws.request("eth_blockNumber", ()).await.unwrap();
|
let block_num2: U256 = ws.request("eth_blockNumber", ()).await.unwrap();
|
||||||
assert!(block_num2 > block_num);
|
assert!(block_num2 > block_num);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
#[cfg(not(feature = "celo"))]
|
||||||
async fn subscription() {
|
async fn subscription() {
|
||||||
|
use ethers_core::types::{Block, TxHash};
|
||||||
|
|
||||||
let anvil = Anvil::new().block_time(1u64).spawn();
|
let anvil = Anvil::new().block_time(1u64).spawn();
|
||||||
let ws = Ws::connect(anvil.ws_endpoint()).await.unwrap();
|
let ws = Ws::connect(anvil.ws_endpoint()).await.unwrap();
|
||||||
|
|
||||||
// Subscribing requires sending the sub request and then subscribing to
|
// Subscribing requires sending the sub request and then subscribing to
|
||||||
// the returned sub_id
|
// the returned sub_id
|
||||||
let sub_id: U256 = ws.request("eth_subscribe", ["newHeads"]).await.unwrap();
|
let sub_id: U256 = ws.request("eth_subscribe", ["newHeads"]).await.unwrap();
|
||||||
let mut stream = ws.subscribe(sub_id).unwrap();
|
let stream = ws.subscribe(sub_id).unwrap();
|
||||||
|
|
||||||
let mut blocks = Vec::new();
|
let blocks: Vec<u64> = stream
|
||||||
for _ in 0..3 {
|
.take(3)
|
||||||
let item = stream.next().await.unwrap();
|
.map(|item| {
|
||||||
let block: Block<TxHash> = serde_json::from_str(item.get()).unwrap();
|
let block: Block<TxHash> = serde_json::from_str(item.get()).unwrap();
|
||||||
blocks.push(block.number.unwrap_or_default().as_u64());
|
block.number.unwrap_or_default().as_u64()
|
||||||
}
|
})
|
||||||
|
.collect()
|
||||||
assert_eq!(blocks, vec![1, 2, 3])
|
.await;
|
||||||
|
assert_eq!(blocks, vec![1, 2, 3]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
#![allow(unused)]
|
#![cfg(not(feature = "celo"))]
|
||||||
|
|
||||||
use ethers_providers::{Middleware, Provider, StreamExt, Ws};
|
use ethers_providers::{Middleware, Provider, StreamExt, Ws};
|
||||||
use futures_util::SinkExt;
|
use futures_util::SinkExt;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
@ -15,14 +16,11 @@ use tungstenite::protocol::Message;
|
||||||
|
|
||||||
const WS_ENDPOINT: &str = "127.0.0.1:9002";
|
const WS_ENDPOINT: &str = "127.0.0.1:9002";
|
||||||
|
|
||||||
#[cfg(not(feature = "celo"))]
|
use ethers_core::types::Filter;
|
||||||
mod eth_tests {
|
use tokio_tungstenite::connect_async;
|
||||||
use super::*;
|
|
||||||
use ethers_core::types::Filter;
|
|
||||||
use tokio_tungstenite::connect_async;
|
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn graceful_disconnect_on_ws_errors() {
|
async fn graceful_disconnect_on_ws_errors() {
|
||||||
// Spawn a fake Ws server that will drop our connection after a while
|
// Spawn a fake Ws server that will drop our connection after a while
|
||||||
spawn_ws_server().await;
|
spawn_ws_server().await;
|
||||||
|
|
||||||
|
@ -32,12 +30,7 @@ mod eth_tests {
|
||||||
let filter = Filter::new().event("Transfer(address,address,uint256)");
|
let filter = Filter::new().event("Transfer(address,address,uint256)");
|
||||||
let mut stream = provider.subscribe_logs(&filter).await.unwrap();
|
let mut stream = provider.subscribe_logs(&filter).await.unwrap();
|
||||||
|
|
||||||
while let Some(_) = stream.next().await {
|
assert!(stream.next().await.is_none());
|
||||||
assert!(false); // force test to fail
|
|
||||||
}
|
|
||||||
|
|
||||||
assert!(true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn spawn_ws_server() {
|
async fn spawn_ws_server() {
|
||||||
|
@ -52,7 +45,7 @@ async fn spawn_ws_server() {
|
||||||
async fn handle_conn(stream: TcpStream) -> Result<(), Error> {
|
async fn handle_conn(stream: TcpStream) -> Result<(), Error> {
|
||||||
let mut ws_stream = accept_async(stream).await?;
|
let mut ws_stream = accept_async(stream).await?;
|
||||||
|
|
||||||
while let Some(_) = ws_stream.next().await {
|
while ws_stream.next().await.is_some() {
|
||||||
let res: String =
|
let res: String =
|
||||||
"{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":\"0xcd0c3e8af590364c09d0fa6a1210faf5\"}"
|
"{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":\"0xcd0c3e8af590364c09d0fa6a1210faf5\"}"
|
||||||
.into();
|
.into();
|
||||||
|
|
Loading…
Reference in New Issue