Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: interprocess-based IPC #59

Merged
merged 13 commits into from
Dec 8, 2023
51 changes: 51 additions & 0 deletions .github/scripts/install_test_binaries.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#!/usr/bin/env bash
# Installs Solc and Geth binaries
# Note: intended for use only with CI (x86_64 Ubuntu, MacOS or Windows)
set -e

GETH_BUILD=${GETH_BUILD:-"1.11.2-73b01f40"}

BIN_DIR=${BIN_DIR:-"$HOME/bin"}

PLATFORM="$(uname -s | awk '{print tolower($0)}')"
if [ "$PLATFORM" != "linux" ] && [ "$PLATFORM" != "darwin" ]; then
EXT=".exe"
fi

main() {
mkdir -p "$BIN_DIR"
cd "$BIN_DIR"
export PATH="$BIN_DIR:$PATH"
if [ "$GITHUB_PATH" ]; then
echo "$BIN_DIR" >> "$GITHUB_PATH"
fi

install_geth

echo ""
echo "Installed Geth:"
geth version
}

# Installs geth from https://geth.ethereum.org/downloads
install_geth() {
case "$PLATFORM" in
linux|darwin)
name="geth-$PLATFORM-amd64-$GETH_BUILD"
curl -s "https://gethstore.blob.core.windows.net/builds/$name.tar.gz" | tar -xzf -
mv -f "$name/geth" ./
rm -rf "$name"
chmod +x geth
;;
*)
name="geth-windows-amd64-$GETH_BUILD"
zip="$name.zip"
curl -so "$zip" "https://gethstore.blob.core.windows.net/builds/$zip"
unzip "$zip"
mv -f "$name/geth.exe" ./
rm -rf "$name" "$zip"
;;
esac
}

main
6 changes: 5 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ jobs:
uses: foundry-rs/foundry-toolchain@v1
with:
version: nightly
- name: Install test binaries
shell: bash
run: ./.github/scripts/install_test_binaries.sh
- uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true
Expand All @@ -54,7 +57,8 @@ jobs:
with:
cache-on-failure: true
- name: check
run: cargo check --workspace --target wasm32-unknown-unknown
# Do not run WASM checks on IPC as it doesn't make sense
run: cargo check --workspace --target wasm32-unknown-unknown --exclude "alloy-transport-ipc"

feature-checks:
runs-on: ubuntu-latest
Expand Down
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ alloy-json-rpc = { version = "0.1.0", path = "crates/json-rpc" }
alloy-transport = { version = "0.1.0", path = "crates/transport" }
alloy-pubsub = { version = "0.1.0", path = "crates/pubsub" }
alloy-transport-http = { version = "0.1.0", path = "crates/transport-http" }
alloy-transport-ipc = { version = "0.1.0", path = "crates/transport-ipc" }
alloy-transport-ws = { version = "0.1.0", path = "crates/transport-ws" }
alloy-networks = { version = "0.1.0", path = "crates/networks" }
alloy-rpc-types = { version = "0.1.0", path = "crates/rpc-types" }
Expand Down
4 changes: 2 additions & 2 deletions crates/json-rpc/src/response/error.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use serde::{
de::{DeserializeOwned, MapAccess, Visitor},
Deserialize, Deserializer,
Deserialize, Deserializer, Serialize,
};
use serde_json::value::RawValue;
use std::{borrow::Borrow, fmt, marker::PhantomData};
Expand All @@ -10,7 +10,7 @@ use std::{borrow::Borrow, fmt, marker::PhantomData};
/// This response indicates that the server received and handled the request,
/// but that there was an error in the processing of it. The error should be
/// included in the `message` field of the response payload.
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Serialize)]
pub struct ErrorPayload<ErrData = Box<RawValue>> {
/// The error code.
pub code: i64,
Expand Down
27 changes: 26 additions & 1 deletion crates/json-rpc/src/response/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use crate::common::Id;
use serde::{
de::{DeserializeOwned, MapAccess, Visitor},
Deserialize, Deserializer,
ser::SerializeMap,
Deserialize, Deserializer, Serialize,
};
use serde_json::value::RawValue;
use std::{borrow::Borrow, fmt, marker::PhantomData};
Expand Down Expand Up @@ -224,6 +225,30 @@ where
}
}

impl<Payload, ErrData> Serialize for Response<Payload, ErrData>
where
Payload: Serialize,
ErrData: Serialize,
{
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut map = serializer.serialize_map(Some(3))?;
map.serialize_entry("jsonrpc", "2.0")?;
map.serialize_entry("id", &self.id)?;
match &self.payload {
ResponsePayload::Success(result) => {
map.serialize_entry("result", result)?;
}
ResponsePayload::Failure(error) => {
map.serialize_entry("error", error)?;
}
}
map.end()
}
}

#[cfg(test)]
mod test {
#[test]
Expand Down
7 changes: 7 additions & 0 deletions crates/rpc-client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,22 @@ reqwest = { workspace = true, optional = true }
tokio = { workspace = true, optional = true }
url = { workspace = true, optional = true }

[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
alloy-transport-ipc = { workspace = true, optional = true }

[dev-dependencies]
alloy-primitives.workspace = true
alloy-transport-ws.workspace = true
test-log = { version = "0.2.13", default-features = false, features = ["trace"] }
tracing-subscriber = { version = "0.3.17", features = ["std", "env-filter"] }
ethers-core = "2.0.10"
alloy-transport-ipc = { workspace = true, features = ["mock"] }
tempfile = "3"

[features]
default = ["reqwest"]
reqwest = ["dep:url", "dep:reqwest", "alloy-transport-http/reqwest"]
hyper = ["dep:url", "dep:hyper", "alloy-transport-http/hyper"]
pubsub = ["dep:tokio", "dep:alloy-pubsub", "dep:alloy-primitives"]
ws = ["pubsub", "dep:alloy-transport-ws"]
ipc = ["pubsub", "dep:alloy-transport-ipc"]
12 changes: 11 additions & 1 deletion crates/rpc-client/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ where
#[cfg(feature = "pubsub")]
mod pubsub_impl {
use super::*;
use alloy_pubsub::PubSubFrontend;
use alloy_pubsub::{PubSubConnect, PubSubFrontend};
use tokio::sync::broadcast;

impl RpcClient<PubSubFrontend> {
Expand All @@ -142,6 +142,16 @@ mod pubsub_impl {
) -> broadcast::Receiver<Box<serde_json::value::RawValue>> {
self.transport.get_subscription(id).await.unwrap()
}

/// Connect to a transport via a [`PubSubConnect`] implementor.
pub async fn connect_pubsub<C>(
connect: C,
) -> Result<RpcClient<PubSubFrontend>, TransportError>
where
C: PubSubConnect,
{
ClientBuilder::default().pubsub(connect).await
}
}
}

Expand Down
6 changes: 6 additions & 0 deletions crates/rpc-client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,9 @@ pub use call::RpcCall;

mod client;
pub use client::RpcClient;

#[cfg(feature = "ws")]
pub use alloy_transport_ws::WsConnect;

#[cfg(all(feature = "ipc", not(target_arch = "wasm32")))]
pub use alloy_transport_ipc::IpcConnect;
38 changes: 38 additions & 0 deletions crates/rpc-client/tests/it/ipc.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
use std::borrow::Cow;

use alloy_primitives::U64;
use alloy_pubsub::PubSubFrontend;
use alloy_rpc_client::{ClientBuilder, RpcCall, RpcClient};
use alloy_transport_ipc::IpcConnect;
use ethers_core::utils::{Geth, GethInstance};
use tempfile::NamedTempFile;

async fn connect() -> (RpcClient<PubSubFrontend>, 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 connector: IpcConnect<_> = path.into();

let client = ClientBuilder::default().pubsub(connector).await.unwrap();

(client, geth)
}

#[test_log::test(tokio::test)]
async fn it_makes_a_request() {
let (client, _geth) = connect().await;

let params: Cow<'static, _> = Cow::Owned(vec![]);

let req: RpcCall<_, Cow<'static, Vec<String>>, U64> = client.prepare("eth_blockNumber", params);

let timeout = tokio::time::timeout(std::time::Duration::from_secs(2), req);

timeout.await.unwrap().unwrap();
}
3 changes: 3 additions & 0 deletions crates/rpc-client/tests/it/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,6 @@ mod http;

#[cfg(feature = "pubsub")]
mod ws;

#[cfg(feature = "pubsub")]
mod ipc;
2 changes: 1 addition & 1 deletion crates/rpc-types/src/eth/filter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -736,7 +736,7 @@ impl FilteredParams {
}

/// Returns `true` if the bloom matches the topics
pub fn matches_topics(bloom: Bloom, topic_filters: &Vec<BloomFilter>) -> bool {
pub fn matches_topics(bloom: Bloom, topic_filters: &[BloomFilter]) -> bool {
if topic_filters.is_empty() {
return true;
}
Expand Down
32 changes: 32 additions & 0 deletions crates/transport-ipc/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
[package]
name = "alloy-transport-ipc"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
authors.workspace = true
license.workspace = true
homepage.workspace = true
repository.workspace = true
exclude.workspace = true

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
alloy-json-rpc.workspace = true
alloy-transport.workspace = true
alloy-pubsub.workspace = true

futures.workspace = true
pin-project.workspace = true
serde_json.workspace = true
tokio.workspace = true
tracing.workspace = true

bytes = "1.5.0"
interprocess = { version = "1.2.1", features = ["tokio", "tokio_support"] }
serde = { workspace = true, optional = true }
tempfile = { version = "3.8.1", optional = true }

[features]
default = []
mock = ["dep:serde", "dep:tempfile"]
3 changes: 3 additions & 0 deletions crates/transport-ipc/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# alloy-transport-ipc

IPC transport implementation.
52 changes: 52 additions & 0 deletions crates/transport-ipc/src/connect.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
use std::{
ffi::{CString, OsString},
path::PathBuf,
};

#[derive(Debug, Clone)]
/// An IPC Connection object.
pub struct IpcConnect<T> {
///
inner: T,
}

macro_rules! impl_connect {
($target:ty) => {
impl From<$target> for IpcConnect<$target> {
fn from(inner: $target) -> Self {
Self { inner }
}
}

impl From<IpcConnect<$target>> for $target {
fn from(this: IpcConnect<$target>) -> $target {
this.inner
}
}

impl alloy_pubsub::PubSubConnect for IpcConnect<$target> {
fn is_local(&self) -> bool {
true
}

fn connect<'a: 'b, 'b>(
&'a self,
) -> alloy_transport::Pbf<
'b,
alloy_pubsub::ConnectionHandle,
alloy_transport::TransportError,
> {
Box::pin(async move {
crate::IpcBackend::connect(&self.inner)
.await
.map_err(alloy_transport::TransportErrorKind::custom)
})
}
}
};
}

impl_connect!(OsString);
impl_connect!(CString);
impl_connect!(PathBuf);
impl_connect!(String);
Loading