From 163cefbcde0ee97c1d409e58b24d4e7f69c76a00 Mon Sep 17 00:00:00 2001 From: Ryan Date: Fri, 14 Jun 2024 10:59:16 +0200 Subject: [PATCH] feat(rpc): Implement WASM Client (#210) --- .github/workflows/ci.yml | 24 +++++++- Cargo.lock | 51 ++++++++++++++++ ci/docker-compose.yml | 20 ++++++- ci/run-bridge.sh | 2 + rpc/Cargo.toml | 9 ++- rpc/src/client.rs | 123 ++++++++++++++++++++++++++++++++++----- rpc/src/lib.rs | 12 +++- rpc/tests/wasm.rs | 26 +++++++++ 8 files changed, 246 insertions(+), 21 deletions(-) create mode 100644 rpc/tests/wasm.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 51f8686a..9c7eeb6b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,5 +1,4 @@ name: CI - on: push: branches: @@ -130,6 +129,20 @@ jobs: version: "23.3" repo-token: ${{ secrets.GITHUB_TOKEN }} + - name: Install Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + target: wasm32-unknown-unknown + + - name: Install wasm-pack + uses: taiki-e/cache-cargo-install-action@v1 + with: + tool: wasm-pack@0.12.1 + + - name: Install chromedriver # we don't specify chrome version to match whatever's installed + uses: nanasess/setup-chromedriver@v2 + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 @@ -152,6 +165,11 @@ jobs: "cache-from": ["type=gha,scope=bridge-0"], "cache-to": ["type=gha,mode=max,scope=bridge-0"], "output": ["type=docker"] + }, + "bridge-1": { + "cache-from": ["type=gha,scope=bridge-1"], + "cache-to": ["type=gha,mode=max,scope=bridge-1"], + "output": ["type=docker"] } } } @@ -172,6 +190,10 @@ jobs: - name: Run tests run: cargo test + - name: Run rpc wasm test + run: wasm-pack test --headless --chrome rpc --features=wasm-bindgen + + unused-deps: runs-on: ubuntu-latest diff --git a/Cargo.lock b/Cargo.lock index ebc1501a..656cf172 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -737,6 +737,7 @@ dependencies = [ "celestia-types", "dotenvy", "futures", + "getrandom", "http", "jsonrpsee", "libp2p", @@ -746,6 +747,7 @@ dependencies = [ "thiserror", "tokio", "tracing", + "wasm-bindgen-test", ] [[package]] @@ -1658,6 +1660,27 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" +[[package]] +name = "gloo-net" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ac9e8288ae2c632fa9f8657ac70bfe38a1530f345282d7ba66a1f70b72b7dc4" +dependencies = [ + "futures-channel", + "futures-core", + "futures-sink", + "gloo-utils", + "http", + "js-sys", + "pin-project", + "serde", + "serde_json", + "thiserror", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "gloo-timers" version = "0.2.6" @@ -1682,6 +1705,19 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "gloo-utils" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "h2" version = "0.3.26" @@ -2085,6 +2121,7 @@ dependencies = [ "jsonrpsee-http-client", "jsonrpsee-proc-macros", "jsonrpsee-types", + "jsonrpsee-wasm-client", "jsonrpsee-ws-client", "tracing", ] @@ -2095,7 +2132,9 @@ version = "0.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5b005c793122d03217da09af68ba9383363caa950b90d3436106df8cabce935" dependencies = [ + "futures-channel", "futures-util", + "gloo-net", "http", "jsonrpsee-core", "pin-project", @@ -2129,6 +2168,7 @@ dependencies = [ "thiserror", "tokio", "tracing", + "wasm-bindgen-futures", ] [[package]] @@ -2178,6 +2218,17 @@ dependencies = [ "tracing", ] +[[package]] +name = "jsonrpsee-wasm-client" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c7cbb3447cf14fd4d2f407c3cc96e6c9634d5440aa1fbed868a31f3c02b27f0" +dependencies = [ + "jsonrpsee-client-transport", + "jsonrpsee-core", + "jsonrpsee-types", +] + [[package]] name = "jsonrpsee-ws-client" version = "0.20.3" diff --git a/ci/docker-compose.yml b/ci/docker-compose.yml index f832f0e0..69f93e6f 100644 --- a/ci/docker-compose.yml +++ b/ci/docker-compose.yml @@ -7,7 +7,7 @@ services: dockerfile: Dockerfile.validator environment: # provide amount of bridge nodes to provision (default: 1) - - BRIDGE_COUNT=1 + - BRIDGE_COUNT=2 volumes: - credentials:/credentials - genesis:/genesis @@ -28,6 +28,24 @@ services: - credentials:/credentials - genesis:/genesis + bridge-1: + image: bridge + platform: "linux/amd64" + build: + context: . + dockerfile: Dockerfile.bridge + environment: + # provide an id for the bridge node (default: 0) + # each node should have a next natural number starting from 0 + - NODE_ID=1 + # setting SKIP_AUTH to true disables the use of JWT for authentication + - SKIP_AUTH=true + ports: + - 36658:26658 + volumes: + - credentials:/credentials + - genesis:/genesis + # Uncomment for another bridge node # remember to adjust services.validator.command # bridge-1: diff --git a/ci/run-bridge.sh b/ci/run-bridge.sh index 0b6b4f70..448d6c35 100755 --- a/ci/run-bridge.sh +++ b/ci/run-bridge.sh @@ -4,6 +4,7 @@ set -euo pipefail # Name for this node or `bridge-0` if not provided NODE_ID="${NODE_ID:-0}" +SKIP_AUTH="${SKIP_AUTH:-false}" NODE_NAME="bridge-$NODE_ID" # a private local network P2P_NETWORK="private" @@ -67,6 +68,7 @@ main() { # Start the bridge node echo "Configuration finished. Running a bridge node..." celestia bridge start \ + --rpc.skip-auth=$SKIP_AUTH \ --rpc.addr 0.0.0.0 \ --core.ip validator \ --keyring.accname "$NODE_NAME" \ diff --git a/rpc/Cargo.toml b/rpc/Cargo.toml index 069a6b52..5ca71701 100644 --- a/rpc/Cargo.toml +++ b/rpc/Cargo.toml @@ -30,7 +30,7 @@ tracing = "0.1.37" http = "0.2.9" jsonrpsee = { version = "0.20", features = ["http-client", "ws-client"] } -[dev-dependencies] +[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] libp2p = { workspace = true, features = [ "tokio", "macros", @@ -38,7 +38,6 @@ libp2p = { workspace = true, features = [ "noise", "yamux", ] } - anyhow = "1.0.71" dotenvy = "0.15.7" futures = "0.3.28" @@ -47,10 +46,14 @@ rand = "0.8.5" tokio = { version = "1.32.0", features = ["rt", "macros"] } tracing = "0.1.37" +[target.'cfg(target_arch = "wasm32")'.dev-dependencies] +getrandom = { version = "0.2", features = ["js"] } +wasm-bindgen-test = "0.3.0" + [features] default = ["p2p"] p2p = ["celestia-types/p2p"] -wasm-bindgen = ["celestia-types/wasm-bindgen"] +wasm-bindgen = ["celestia-types/wasm-bindgen", "jsonrpsee/wasm-client"] [package.metadata.docs.rs] features = ["p2p"] diff --git a/rpc/src/client.rs b/rpc/src/client.rs index 3bca86c2..eee8be0c 100644 --- a/rpc/src/client.rs +++ b/rpc/src/client.rs @@ -7,12 +7,13 @@ #[cfg(not(target_arch = "wasm32"))] pub use self::native::Client; +#[cfg(all(target_arch = "wasm32", feature = "wasm-bindgen"))] +pub use self::wasm::Client; + #[cfg(not(target_arch = "wasm32"))] mod native { - use std::fmt; - use std::result::Result as StdResult; + use std::{fmt, result::Result}; - use crate::{Error, Result}; use async_trait::async_trait; use http::{header, HeaderValue}; use jsonrpsee::core::client::{BatchResponse, ClientT, Subscription, SubscriptionClientT}; @@ -23,6 +24,8 @@ mod native { use jsonrpsee::ws_client::{WsClient, WsClientBuilder}; use serde::de::DeserializeOwned; + use crate::Error; + /// Json RPC client. pub enum Client { /// A client using 'http\[s\]' protocol. @@ -40,7 +43,7 @@ mod native { /// /// Please note that currently the celestia-node supports only 'http' and 'ws'. /// For a secure connection you have to hide it behind a proxy. - pub async fn new(conn_str: &str, auth_token: Option<&str>) -> Result { + pub async fn new(conn_str: &str, auth_token: Option<&str>) -> Result { let mut headers = HeaderMap::new(); if let Some(token) = auth_token { @@ -70,11 +73,7 @@ mod native { #[async_trait] impl ClientT for Client { - async fn notification( - &self, - method: &str, - params: Params, - ) -> StdResult<(), JrpcError> + async fn notification(&self, method: &str, params: Params) -> Result<(), JrpcError> where Params: ToRpcParams + Send, { @@ -84,7 +83,7 @@ mod native { } } - async fn request(&self, method: &str, params: Params) -> StdResult + async fn request(&self, method: &str, params: Params) -> Result where R: DeserializeOwned, Params: ToRpcParams + Send, @@ -98,7 +97,7 @@ mod native { async fn batch_request<'a, R>( &self, batch: BatchRequestBuilder<'a>, - ) -> StdResult, JrpcError> + ) -> Result, JrpcError> where R: DeserializeOwned + fmt::Debug + 'a, { @@ -116,7 +115,7 @@ mod native { subscribe_method: &'a str, params: Params, unsubscribe_method: &'a str, - ) -> StdResult, JrpcError> + ) -> Result, JrpcError> where Params: ToRpcParams + Send, N: DeserializeOwned, @@ -138,7 +137,7 @@ mod native { async fn subscribe_to_method<'a, N>( &self, method: &'a str, - ) -> StdResult, JrpcError> + ) -> Result, JrpcError> where N: DeserializeOwned, { @@ -150,7 +149,101 @@ mod native { } } -#[cfg(target_arch = "wasm32")] +#[cfg(all(target_arch = "wasm32", feature = "wasm-bindgen"))] mod wasm { - // TODO: implement HttpClient with `fetch` + use std::{fmt, result::Result}; + + use async_trait::async_trait; + use jsonrpsee::core::client::{BatchResponse, ClientT, Subscription, SubscriptionClientT}; + use jsonrpsee::core::params::BatchRequestBuilder; + use jsonrpsee::core::traits::ToRpcParams; + use jsonrpsee::core::Error as JrpcError; + use jsonrpsee::wasm_client::{Client as WasmClient, WasmClientBuilder}; + use serde::de::DeserializeOwned; + + use crate::Error; + + /// Json RPC client. + pub struct Client { + client: WasmClient, + } + + impl Client { + /// Create a new Json RPC client. + /// + /// Only the 'ws\[s\]' protocols are supported and they should + /// be specified in the provided `conn_str`. For more flexibility + /// consider creating the client using [`jsonrpsee`] directly. + /// + /// Since headers are not supported in the current version of + /// `jsonrpsee-wasm-client`, celestia-node requires disabling + /// authentication (--rpc.skip-auth) to use wasm. + /// + /// For a secure connection you have to hide it behind a proxy. + pub async fn new(conn_str: &str) -> Result { + let protocol = conn_str.split_once(':').map(|(proto, _)| proto); + let client = match protocol { + Some("ws") | Some("wss") => WasmClientBuilder::default().build(conn_str).await?, + _ => return Err(Error::ProtocolNotSupported(conn_str.into())), + }; + + Ok(Client { client }) + } + } + + #[async_trait] + impl ClientT for Client { + async fn notification(&self, method: &str, params: Params) -> Result<(), JrpcError> + where + Params: ToRpcParams + Send, + { + self.client.notification(method, params).await + } + + async fn request(&self, method: &str, params: Params) -> Result + where + R: DeserializeOwned, + Params: ToRpcParams + Send, + { + self.client.request(method, params).await + } + + async fn batch_request<'a, R>( + &self, + batch: BatchRequestBuilder<'a>, + ) -> Result, JrpcError> + where + R: DeserializeOwned + fmt::Debug + 'a, + { + self.client.batch_request(batch).await + } + } + + #[async_trait] + impl SubscriptionClientT for Client { + async fn subscribe<'a, N, Params>( + &self, + subscribe_method: &'a str, + params: Params, + unsubscribe_method: &'a str, + ) -> Result, JrpcError> + where + Params: ToRpcParams + Send, + N: DeserializeOwned, + { + self.client + .subscribe(subscribe_method, params, unsubscribe_method) + .await + } + + async fn subscribe_to_method<'a, N>( + &self, + method: &'a str, + ) -> Result, JrpcError> + where + N: DeserializeOwned, + { + self.client.subscribe_to_method(method).await + } + } } diff --git a/rpc/src/lib.rs b/rpc/src/lib.rs index a5f88a57..a119a475 100644 --- a/rpc/src/lib.rs +++ b/rpc/src/lib.rs @@ -11,7 +11,17 @@ mod share; mod state; pub use crate::blob::BlobClient; -#[cfg(not(target_arch = "wasm32"))] +#[cfg(any( + not(target_arch = "wasm32"), + all(target_arch = "wasm32", feature = "wasm-bindgen") +))] +#[cfg_attr( + docsrs, + doc(cfg(any( + not(target_arch = "wasm32"), + all(target_arch = "wasm32", feature = "wasm-bindgen") + ))) +)] pub use crate::client::Client; pub use crate::error::{Error, Result}; pub use crate::header::HeaderClient; diff --git a/rpc/tests/wasm.rs b/rpc/tests/wasm.rs new file mode 100644 index 00000000..bb29c1a1 --- /dev/null +++ b/rpc/tests/wasm.rs @@ -0,0 +1,26 @@ +#![cfg(all(target_arch = "wasm32", feature = "wasm-bindgen"))] + +use celestia_rpc::client::Client; +use celestia_rpc::prelude::*; +use wasm_bindgen_test::*; + +// uses bridge-1, which has skip-auth enabled +const CELESTIA_RPC_URL: &str = "ws://localhost:36658"; + +wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); + +#[wasm_bindgen_test] +async fn network_head() { + let client = Client::new(CELESTIA_RPC_URL).await.unwrap(); + let network_head = client.header_network_head().await.unwrap(); + + let genesis_header = client.header_get_by_height(1).await.unwrap(); + let adjacent_header = client + .header_get_by_height(network_head.height().value() - 1) + .await + .unwrap(); + + network_head.validate().unwrap(); + genesis_header.verify(&network_head).unwrap(); + adjacent_header.verify(&network_head).unwrap(); +}