diff --git a/Cargo.lock b/Cargo.lock index fd22013..eb9300c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5710,6 +5710,7 @@ version = "0.0.0" dependencies = [ "alloy-primitives", "anyhow", + "async-trait", "axum 0.8.4", "commonware-codec", "commonware-consensus", @@ -5719,6 +5720,7 @@ dependencies = [ "dirs 5.0.1", "ethereum_ssz", "futures", + "reqwest", "serde", "serde_json", "summit-finalizer", diff --git a/rpc/Cargo.toml b/rpc/Cargo.toml index 4dcb17a..de03817 100644 --- a/rpc/Cargo.toml +++ b/rpc/Cargo.toml @@ -26,3 +26,5 @@ dirs = "5.0.1" serde = { workspace = true } serde_json = { workspace = true } tracing.workspace = true +async-trait = "0.1" +reqwest = { workspace = true, features = ["json"] } diff --git a/rpc/src/client/client.rs b/rpc/src/client/client.rs new file mode 100644 index 0000000..ee5a2a0 --- /dev/null +++ b/rpc/src/client/client.rs @@ -0,0 +1,138 @@ +use summit_types::rpc::{ + CheckpointInfoRes, CheckpointRes, DepositTransactionResponse, PublicKeysResponse, + ServerModeResponse, +}; + +use crate::client::{traits::*, types::ClientError}; + +/// HTTP client for Summit RPC +pub struct Client { + base_url: String, + client: reqwest::Client, +} + +impl Client { + /// Create a new client with the given base URL + pub fn new(url: impl Into) -> Self { + Self { + base_url: url.into(), + client: reqwest::Client::new(), + } + } + + fn url(&self, path: &str) -> String { + format!( + "{}/{}", + self.base_url.trim_end_matches('/'), + path.trim_start_matches('/') + ) + } +} + +#[async_trait::async_trait] +impl SummitClient for Client { + async fn health(&self) -> Result { + self.client + .get(self.url("health")) + .send() + .await? + .text() + .await + } + + async fn server_mode(&self) -> Result { + self.client + .get(self.url("server_mode")) + .send() + .await? + .json() + .await + } + + async fn get_public_keys(&self) -> Result { + self.client + .get(self.url("get_public_keys")) + .send() + .await? + .json() + .await + } +} + +#[async_trait::async_trait] +impl GenesisClient for Client { + async fn send_genesis(&self, body: String) -> Result { + self.client + .post(self.url("send_genesis")) + .body(body) + .send() + .await? + .text() + .await + } +} + +#[async_trait::async_trait] +impl NodeClient for Client { + async fn get_checkpoint(&self, epoch: u64) -> Result { + self.client + .get(self.url(&format!("get_checkpoint/{}", epoch))) + .send() + .await? + .json() + .await + } + + async fn get_latest_checkpoint(&self) -> Result { + self.client + .get(self.url("get_latest_checkpoint")) + .send() + .await? + .json() + .await + } + + async fn get_latest_checkpoint_info(&self) -> Result { + self.client + .get(self.url("get_latest_checkpoint_info")) + .send() + .await? + .json() + .await + } + + async fn get_latest_height(&self) -> Result { + let text = self + .client + .get(self.url("get_latest_height")) + .send() + .await? + .text() + .await?; + Ok(text.parse()?) + } + + async fn get_validator_balance(&self, public_key: &str) -> Result { + let text = self + .client + .get(self.url(&format!("get_validator_balance?public_key={}", public_key))) + .send() + .await? + .text() + .await?; + Ok(text.parse()?) + } + + async fn get_deposit_signature( + &self, + amount: u64, + address: &str, + ) -> Result { + self.client + .get(self.url(&format!("get_deposit_signature/{}/{}", amount, address))) + .send() + .await? + .json() + .await + } +} diff --git a/rpc/src/client/mod.rs b/rpc/src/client/mod.rs new file mode 100644 index 0000000..4a7fad3 --- /dev/null +++ b/rpc/src/client/mod.rs @@ -0,0 +1,36 @@ +//! Summit RPC client +//! +//! # Example +//! +//! ```no_run +//! use summit_rpc::client::{Client, SummitClient, NodeClient, GenesisClient}; +//! +//! #[tokio::main] +//! async fn main() { +//! let client = Client::new("http://localhost:8080"); +//! +//! // Check server mode +//! let mode = client.server_mode().await.unwrap(); +//! +//! // Use node-specific methods +//! let height = client.get_latest_height().await.unwrap(); +//! +//! // Use genesis-specific methods +//! let genesis = std::fs::read_to_string("genesis.json").unwrap(); +//! client.send_genesis(genesis).await.unwrap(); +//! } +//! ``` + +mod client; +mod traits; +mod types; + +pub use client::*; +pub use traits::*; +pub use types::*; + +// Re-export API response types from summit_types for convenience +pub use summit_types::rpc::{ + CheckpointInfoRes, CheckpointRes, DepositTransactionResponse, PublicKeysResponse, ServerMode, + ServerModeResponse, +}; diff --git a/rpc/src/client/traits.rs b/rpc/src/client/traits.rs new file mode 100644 index 0000000..452269c --- /dev/null +++ b/rpc/src/client/traits.rs @@ -0,0 +1,52 @@ +use summit_types::rpc::{ + CheckpointInfoRes, CheckpointRes, DepositTransactionResponse, PublicKeysResponse, + ServerModeResponse, +}; + +use crate::client::types::ClientError; + +/// Shared client methods available in both genesis and node modes +#[async_trait::async_trait] +pub trait SummitClient { + /// Health check + async fn health(&self) -> Result; + + /// Get server mode (genesis or node) + async fn server_mode(&self) -> Result; + + /// Get node and consensus public keys + async fn get_public_keys(&self) -> Result; +} + +/// Genesis-specific client methods +#[async_trait::async_trait] +pub trait GenesisClient: SummitClient { + /// Send genesis file + async fn send_genesis(&self, body: String) -> Result; +} + +/// Node-specific client methods +#[async_trait::async_trait] +pub trait NodeClient: SummitClient { + /// Get checkpoint by epoch + async fn get_checkpoint(&self, epoch: u64) -> Result; + + /// Get latest checkpoint + async fn get_latest_checkpoint(&self) -> Result; + + /// Get latest checkpoint info + async fn get_latest_checkpoint_info(&self) -> Result; + + /// Get latest height + async fn get_latest_height(&self) -> Result; + + /// Get validator balance by public key + async fn get_validator_balance(&self, public_key: &str) -> Result; + + /// Get deposit signature + async fn get_deposit_signature( + &self, + amount: u64, + address: &str, + ) -> Result; +} diff --git a/rpc/src/client/types.rs b/rpc/src/client/types.rs new file mode 100644 index 0000000..79a6194 --- /dev/null +++ b/rpc/src/client/types.rs @@ -0,0 +1,30 @@ +use std::num::ParseIntError; + +#[derive(Debug)] +pub enum ClientError { + Request(reqwest::Error), + Parse(ParseIntError), +} + +impl From for ClientError { + fn from(e: reqwest::Error) -> Self { + ClientError::Request(e) + } +} + +impl From for ClientError { + fn from(e: ParseIntError) -> Self { + ClientError::Parse(e) + } +} + +impl std::fmt::Display for ClientError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ClientError::Request(e) => write!(f, "request error: {}", e), + ClientError::Parse(e) => write!(f, "parse error: {}", e), + } + } +} + +impl std::error::Error for ClientError {} diff --git a/rpc/src/lib.rs b/rpc/src/lib.rs index 2550d3e..c6b0178 100644 --- a/rpc/src/lib.rs +++ b/rpc/src/lib.rs @@ -1,3 +1,4 @@ +pub mod client; pub mod routes; use crate::routes::RpcRoutes; use commonware_consensus::Block as ConsensusBlock; diff --git a/rpc/src/routes.rs b/rpc/src/routes.rs index 4e5bf2b..0f74c0b 100644 --- a/rpc/src/routes.rs +++ b/rpc/src/routes.rs @@ -12,36 +12,23 @@ use commonware_consensus::Block as ConsensusBlock; use commonware_consensus::simplex::signing_scheme::Scheme; use commonware_cryptography::{Committable, Hasher as _, Sha256, Signer as _}; use commonware_utils::from_hex_formatted; -use serde::{Deserialize, Serialize}; +use serde::Deserialize; use ssz::Encode; use summit_types::{ KeyPaths, PROTOCOL_VERSION, PublicKey, execution_request::{DepositRequest, compute_deposit_data_root}, - rpc::{CheckpointInfoRes, CheckpointRes}, + rpc::{ + CheckpointInfoRes, CheckpointRes, DepositTransactionResponse, PublicKeysResponse, + ServerMode, ServerModeResponse, + }, utils::get_expanded_path, }; -#[derive(Serialize)] -struct PublicKeysResponse { - node: String, - consensus: String, -} - #[derive(Deserialize)] struct ValidatorBalanceQuery { public_key: String, } -#[derive(Serialize, Deserialize)] -struct DepositTransactionResponse { - node_pubkey: [u8; 32], - consensus_pubkey: Vec, // 48 bytes - withdrawal_credentials: [u8; 32], - node_signature: Vec, // 48 bytes - consensus_signature: Vec, // 96 bytes - deposit_data_root: [u8; 32], -} - pub(crate) struct RpcRoutes; impl RpcRoutes { @@ -53,6 +40,7 @@ impl RpcRoutes { Router::new() .route("/health", get(Self::handle_health_check)) + .route("/server_mode", get(Self::handle_server_mode)) .route("/get_public_keys", get(Self::handle_get_pub_keys::)) .route( "/get_checkpoint/{epoch}", @@ -87,6 +75,7 @@ impl RpcRoutes { Router::new() .route("/health", get(Self::handle_health_check)) + .route("/server_mode", get(Self::handle_server_mode_genesis)) .route("/get_public_keys", get(Self::handle_get_pub_keys_genesis)) .route("/send_genesis", post(Self::handle_send_genesis)) .with_state(state) @@ -96,6 +85,18 @@ impl RpcRoutes { "Ok" } + async fn handle_server_mode() -> Json { + Json(ServerModeResponse { + mode: ServerMode::Node, + }) + } + + async fn handle_server_mode_genesis() -> Json { + Json(ServerModeResponse { + mode: ServerMode::Genesis, + }) + } + async fn handle_get_pub_keys( State(state): State>>, ) -> Result { diff --git a/types/src/rpc.rs b/types/src/rpc.rs index 7905507..267984e 100644 --- a/types/src/rpc.rs +++ b/types/src/rpc.rs @@ -1,5 +1,23 @@ use serde::{Deserialize, Serialize}; +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum ServerMode { + Genesis, + Node, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct ServerModeResponse { + pub mode: ServerMode, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct PublicKeysResponse { + pub node: String, + pub consensus: String, +} + #[derive(Debug, Serialize, Deserialize, Clone)] pub struct CheckpointRes { pub checkpoint: Vec, @@ -12,3 +30,13 @@ pub struct CheckpointInfoRes { pub epoch: u64, pub digest: [u8; 32], } + +#[derive(Debug, Serialize, Deserialize)] +pub struct DepositTransactionResponse { + pub node_pubkey: [u8; 32], + pub consensus_pubkey: Vec, // 48 bytes + pub withdrawal_credentials: [u8; 32], + pub node_signature: Vec, // 48 bytes + pub consensus_signature: Vec, // 96 bytes + pub deposit_data_root: [u8; 32], +}