From 5dcfd7ce74cc3d65b9f69591b242d17b0adf07de Mon Sep 17 00:00:00 2001 From: Brian Anderson Date: Tue, 20 Jul 2021 13:49:32 -0500 Subject: [PATCH] Add some docs for RpcClient and friends (#18748) * Add some docs for RpcSender, HttpSender, MockSender * Support SimulateTransaction in MockSender * Add docs for RpcClient constructors * Add some more RpcClient examples * rustfmt * Reflow docs in rpc_client and friends --- client/src/http_sender.rs | 10 + client/src/mock_sender.rs | 39 +++- client/src/rpc_client.rs | 413 ++++++++++++++++++++++++++++++++++++++ client/src/rpc_sender.rs | 13 ++ 4 files changed, 474 insertions(+), 1 deletion(-) diff --git a/client/src/http_sender.rs b/client/src/http_sender.rs index 9c3f98a9b5..76c31895ac 100644 --- a/client/src/http_sender.rs +++ b/client/src/http_sender.rs @@ -1,3 +1,5 @@ +//! The standard [`RpcSender`] over HTTP. + use { crate::{ client_error::Result, @@ -28,11 +30,19 @@ pub struct HttpSender { request_id: AtomicU64, } +/// The standard [`RpcSender`] over HTTP. impl HttpSender { + /// Create an HTTP RPC sender. + /// + /// The URL is an HTTP URL, usually for port 8899, as in + /// "http://localhost:8899". The sender has a default timeout of 30 seconds. pub fn new(url: String) -> Self { Self::new_with_timeout(url, Duration::from_secs(30)) } + /// Create an HTTP RPC sender. + /// + /// The URL is an HTTP URL, usually for port 8899. pub fn new_with_timeout(url: String, timeout: Duration) -> Self { // `reqwest::blocking::Client` panics if run in a tokio async context. Shuttle the // request to a different tokio thread to avoid this diff --git a/client/src/mock_sender.rs b/client/src/mock_sender.rs index e40e456745..05928b524f 100644 --- a/client/src/mock_sender.rs +++ b/client/src/mock_sender.rs @@ -1,8 +1,12 @@ +//! An [`RpcSender`] used for unit testing [`RpcClient`](crate::rpc_client::RpcClient). + use { crate::{ client_error::Result, rpc_request::RpcRequest, - rpc_response::{Response, RpcResponseContext, RpcVersionInfo}, + rpc_response::{ + Response, RpcResponseContext, RpcSimulateTransactionResult, RpcVersionInfo, + }, rpc_sender::RpcSender, }, serde_json::{json, Number, Value}, @@ -28,6 +32,31 @@ pub struct MockSender { url: String, } +/// An [`RpcSender`] used for unit testing [`RpcClient`](crate::rpc_client::RpcClient). +/// +/// This is primarily for internal use. +/// +/// Unless directed otherwise, it will generally return a reasonable default +/// response, at least for [`RpcRequest`] values for which responses have been +/// implemented. +/// +/// The behavior can be customized in two ways: +/// +/// 1) The `url` constructor argument is not actually a URL, but a simple string +/// directive that changes `MockSender`s behavior in specific scenarios. +/// +/// If `url` is "fails" then any call to `send` will return `Ok(Value::Null)`. +/// +/// It is customary to set the `url` to "succeeds" for mocks that should +/// return sucessfully, though this value is not actually interpreted. +/// +/// Other possible values of `url` are specific to different `RpcRequest` +/// values. Read the implementation for specifics. +/// +/// 2) Custom responses can be configured by providing [`Mocks`] to the +/// [`MockSender::new_with_mocks`] constructor. This type is a [`HashMap`] +/// from [`RpcRequest`] to a JSON [`Value`] response, Any entries in this map +/// override the default behavior for the given request. impl MockSender { pub fn new(url: String) -> Self { Self::new_with_mocks(url, Mocks::default()) @@ -137,6 +166,14 @@ impl RpcSender for MockSender { }; Value::String(signature) } + RpcRequest::SimulateTransaction => serde_json::to_value(Response { + context: RpcResponseContext { slot: 1 }, + value: RpcSimulateTransactionResult { + err: None, + logs: None, + accounts: None, + }, + })?, RpcRequest::GetMinimumBalanceForRentExemption => Value::Number(Number::from(20)), RpcRequest::GetVersion => { let version = Version::default(); diff --git a/client/src/rpc_client.rs b/client/src/rpc_client.rs index cc4c661df0..ec86c2e03c 100644 --- a/client/src/rpc_client.rs +++ b/client/src/rpc_client.rs @@ -1,3 +1,11 @@ +//! Communication with a Solana node over RPC. +//! +//! Software that interacts with the Solana blockchain, whether querying its +//! state or submitting transactions, communicates with a Solana node over +//! [JSON-RPC], using the [`RpcClient`] type. +//! +//! [JSON-RPC]: https://www.jsonrpc.org/specification + #[allow(deprecated)] use crate::rpc_deprecated_config::{ RpcConfirmedBlockConfig, RpcConfirmedTransactionConfig, @@ -64,6 +72,40 @@ impl RpcClientConfig { } } +/// A client of a remote Solana node. +/// +/// `RpcClient` communicates with a Solana node over [JSON-RPC], with the +/// [Solana JSON-RPC protocol][jsonprot]. It is the primary Rust interface for +/// querying and transacting with the network from external programs. +/// +/// `RpcClient`s generally communicate over HTTP on port 8899, a typical server +/// URL being "http://localhost:8899". +/// +/// By default, requests to confirm transactions are only completed once those +/// transactions are finalized, meaning they are definitely permanently +/// committed. Transactions can be confirmed with less finality by creating +/// `RpcClient` with an explicit [`CommitmentConfig`], or by calling the various +/// `_with_commitment` methods, like +/// [`RpcClient::confirm_transaction_with_commitment`]. +/// +/// Requests may timeout, in which case they return a [`ClientError`] where the +/// [`ClientErrorKind`] is [`ClientErrorKind::Reqwest`], and where the interior +/// [`reqwest::Error`](crate::client_error::reqwest::Error)s +/// [`is_timeout`](crate::client_error::reqwest::Error::is_timeout) method +/// returns `true`. The default timeout is 30 seconds, and may be changed by +/// calling an appropriate constructor with a `timeout` parameter. +/// +/// `RpcClient` encapsulates an [`RpcSender`], which implements the underlying +/// RPC protocol. On top of `RpcSender` it adds methods for common tasks, while +/// re-exposing the underlying RPC sending functionality through the +/// [`send`][RpcClient::send] method. +/// +/// [jsonprot]: https://docs.solana.com/developing/clients/jsonrpc-api +/// [JSON-RPC]: https://www.jsonrpc.org/specification +/// +/// While `RpcClient` encapsulates an abstract `RpcSender`, it is most commonly +/// created with an [`HttpSender`], communicating over HTTP, usually on port +/// 8899. It can also be created with [`MockSender`] during testing. pub struct RpcClient { sender: Box, config: RpcClientConfig, @@ -71,6 +113,12 @@ pub struct RpcClient { } impl RpcClient { + /// Create an `RpcClient` from an [`RpcSender`] and an [`RpcClientConfig`]. + /// + /// This is the basic constructor, allowing construction with any type of + /// `RpcSender`. Most applications should use one of the other constructors, + /// such as [`new`] and [`new_mock`], which create an `RpcClient` + /// encapsulating an [`HttpSender`] and [`MockSender`] respectively. fn new_sender( sender: T, config: RpcClientConfig, @@ -82,10 +130,42 @@ impl RpcClient { } } + /// Create an HTTP `RpcClient`. + /// + /// The URL is an HTTP URL, usually for port 8899, as in + /// "http://localhost:8899". + /// + /// The client has a default timeout of 30 seconds, and a default commitment + /// level of [`Finalized`](CommitmentLevel::Finalized). + /// + /// # Examples + /// + /// ``` + /// # use solana_client::rpc_client::RpcClient; + /// let url = "http://localhost:8899".to_string(); + /// let client = RpcClient::new(url); + /// ``` pub fn new(url: String) -> Self { Self::new_with_commitment(url, CommitmentConfig::default()) } + /// Create an HTTP `RpcClient` with specified commitment level. + /// + /// The URL is an HTTP URL, usually for port 8899, as in + /// "http://localhost:8899". + /// + /// The client has a default timeout of 30 seconds, and a user-specified + /// [`CommitmentLevel`] via [`CommitmentConfig`]. + /// + /// # Examples + /// + /// ``` + /// # use solana_sdk::commitment_config::CommitmentConfig; + /// # use solana_client::rpc_client::RpcClient; + /// let url = "http://localhost:8899".to_string(); + /// let commitment_config = CommitmentConfig::processed(); + /// let client = RpcClient::new_with_commitment(url, commitment_config); + /// ``` pub fn new_with_commitment(url: String, commitment_config: CommitmentConfig) -> Self { Self::new_sender( HttpSender::new(url), @@ -93,6 +173,23 @@ impl RpcClient { ) } + /// Create an HTTP `RpcClient` with specified timeout. + /// + /// The URL is an HTTP URL, usually for port 8899, as in + /// "http://localhost:8899". + /// + /// The client has and a default commitment level of + /// [`Finalized`](CommitmentLevel::Finalized). + /// + /// # Examples + /// + /// ``` + /// # use std::time::Duration; + /// # use solana_client::rpc_client::RpcClient; + /// let url = "http://localhost::8899".to_string(); + /// let timeout = Duration::from_secs(1); + /// let client = RpcClient::new_with_timeout(url, timeout); + /// ``` pub fn new_with_timeout(url: String, timeout: Duration) -> Self { Self::new_sender( HttpSender::new_with_timeout(url, timeout), @@ -100,6 +197,26 @@ impl RpcClient { ) } + /// Create an HTTP `RpcClient` with specified timeout and commitment level. + /// + /// The URL is an HTTP URL, usually for port 8899, as in + /// "http://localhost:8899". + /// + /// # Examples + /// + /// ``` + /// # use std::time::Duration; + /// # use solana_client::rpc_client::RpcClient; + /// # use solana_sdk::commitment_config::CommitmentConfig; + /// let url = "http://localhost::8899".to_string(); + /// let timeout = Duration::from_secs(1); + /// let commitment_config = CommitmentConfig::processed(); + /// let client = RpcClient::new_with_timeout_and_commitment( + /// url, + /// timeout, + /// commitment_config, + /// ); + /// ``` pub fn new_with_timeout_and_commitment( url: String, timeout: Duration, @@ -111,6 +228,36 @@ impl RpcClient { ) } + /// Create an HTTP `RpcClient` with specified timeout and commitment level. + /// + /// The URL is an HTTP URL, usually for port 8899, as in + /// "http://localhost:8899". + /// + /// The `confirm_transaction_initial_timeout` argument specifies, when + /// confirming a transaction via one of the `_with_spinner` methods, like + /// [`RpcClient::send_and_confirm_transaction_with_spinner`], the amount of + /// time to allow for the server to initially process a transaction. In + /// other words, setting `confirm_transaction_initial_timeout` to > 0 allows + /// `RpcClient` to wait for confirmation of a transaction that the server + /// has not "seen" yet. + /// + /// # Examples + /// + /// ``` + /// # use std::time::Duration; + /// # use solana_client::rpc_client::RpcClient; + /// # use solana_sdk::commitment_config::CommitmentConfig; + /// let url = "http://localhost::8899".to_string(); + /// let timeout = Duration::from_secs(1); + /// let commitment_config = CommitmentConfig::processed(); + /// let confirm_transaction_initial_timeout = Duration::from_secs(10); + /// let client = RpcClient::new_with_timeouts_and_commitment( + /// url, + /// timeout, + /// commitment_config, + /// confirm_transaction_initial_timeout, + /// ); + /// ``` pub fn new_with_timeouts_and_commitment( url: String, timeout: Duration, @@ -126,6 +273,26 @@ impl RpcClient { ) } + /// Create a mock `RpcClient`. + /// + /// See the [`MockSender`] documentation for an explanation of + /// how it treats the `url` argument. + /// + /// # Examples + /// + /// ``` + /// # use solana_client::rpc_client::RpcClient; + /// // Create an `RpcClient` that always succeeds + /// let url = "succeeds".to_string(); + /// let successful_client = RpcClient::new_mock(url); + /// ``` + /// + /// ``` + /// # use solana_client::rpc_client::RpcClient; + /// // Create an `RpcClient` that always fails + /// let url = "fails".to_string(); + /// let successful_client = RpcClient::new_mock(url); + /// ``` pub fn new_mock(url: String) -> Self { Self::new_sender( MockSender::new(url), @@ -133,6 +300,34 @@ impl RpcClient { ) } + /// Create a mock `RpcClient`. + /// + /// See the [`MockSender`] documentation for an explanation of how it treats + /// the `url` argument. + /// + /// # Examples + /// + /// ``` + /// # use solana_client::{ + /// # rpc_client::RpcClient, + /// # rpc_request::RpcRequest, + /// # }; + /// # use std::collections::HashMap; + /// # use serde_json::json; + /// use solana_client::rpc_response::{Response, RpcResponseContext}; + /// + /// // Create a mock with a custom repsonse to the `GetBalance` request + /// let account_balance = 50; + /// let account_balance_response = json!(Response { + /// context: RpcResponseContext { slot: 1 }, + /// value: json!(account_balance), + /// }); + /// + /// let mut mocks = HashMap::new(); + /// mocks.insert(RpcRequest::GetBalance, account_balance_response); + /// let url = "succeeds".to_string(); + /// let client = RpcClient::new_mock_with_mocks(url, mocks); + /// ``` pub fn new_mock_with_mocks(url: String, mocks: Mocks) -> Self { Self::new_sender( MockSender::new_with_mocks(url, mocks), @@ -140,10 +335,41 @@ impl RpcClient { ) } + /// Create an HTTP `RpcClient` from a [`SocketAddr`]. + /// + /// The client has a default timeout of 30 seconds, and a default commitment + /// level of [`Finalized`](CommitmentLevel::Finalized). + /// + /// # Examples + /// + /// ``` + /// # use std::net::SocketAddr; + /// # use solana_client::rpc_client::RpcClient; + /// let addr = SocketAddr::from(([127, 0, 0, 1], 8899)); + /// let client = RpcClient::new_socket(addr); + /// ``` pub fn new_socket(addr: SocketAddr) -> Self { Self::new(get_rpc_request_str(addr, false)) } + /// Create an HTTP `RpcClient` from a [`SocketAddr`] with specified commitment level. + /// + /// The client has a default timeout of 30 seconds, and a user-specified + /// [`CommitmentLevel`] via [`CommitmentConfig`]. + /// + /// # Examples + /// + /// ``` + /// # use std::net::SocketAddr; + /// # use solana_client::rpc_client::RpcClient; + /// # use solana_sdk::commitment_config::CommitmentConfig; + /// let addr = SocketAddr::from(([127, 0, 0, 1], 8899)); + /// let commitment_config = CommitmentConfig::processed(); + /// let client = RpcClient::new_socket_with_commitment( + /// addr, + /// commitment_config + /// ); + /// ``` pub fn new_socket_with_commitment( addr: SocketAddr, commitment_config: CommitmentConfig, @@ -151,6 +377,20 @@ impl RpcClient { Self::new_with_commitment(get_rpc_request_str(addr, false), commitment_config) } + /// Create an HTTP `RpcClient` from a [`SocketAddr`] with specified timeout. + /// + /// The client has and a default commitment level of [`Finalized`](CommitmentLevel::Finalized). + /// + /// # Examples + /// + /// ``` + /// # use std::net::SocketAddr; + /// # use std::time::Duration; + /// # use solana_client::rpc_client::RpcClient; + /// let addr = SocketAddr::from(([127, 0, 0, 1], 8899)); + /// let timeout = Duration::from_secs(1); + /// let client = RpcClient::new_socket_with_timeout(addr, timeout); + /// ``` pub fn new_socket_with_timeout(addr: SocketAddr, timeout: Duration) -> Self { let url = get_rpc_request_str(addr, false); Self::new_with_timeout(url, timeout) @@ -215,12 +455,69 @@ impl RpcClient { Ok(request) } + /// # Examples + /// + /// ``` + /// # use solana_client::{ + /// # client_error::ClientError, + /// # rpc_client::RpcClient, + /// # rpc_config::RpcSimulateTransactionConfig, + /// # }; + /// # use solana_sdk::{ + /// # signature::Signature, + /// # signer::keypair::Keypair, + /// # hash::Hash, + /// # system_transaction, + /// # }; + /// # let rpc_client = RpcClient::new_mock("succeeds".to_string()); + /// // Transfer lamports from some account to a random account + /// let key = Keypair::new(); + /// let to = solana_sdk::pubkey::new_rand(); + /// let lamports = 50; + /// # let recent_blockhash = Hash::default(); + /// let tx = system_transaction::transfer(&key, &to, lamports, recent_blockhash); + /// let signature = rpc_client.send_transaction(&tx)?; + /// let confirmed = rpc_client.confirm_transaction(&signature)?; + /// assert!(confirmed); + /// # Ok::<(), ClientError>(()) + /// ``` pub fn confirm_transaction(&self, signature: &Signature) -> ClientResult { Ok(self .confirm_transaction_with_commitment(signature, self.commitment())? .value) } + /// # Examples + /// + /// ``` + /// # use solana_client::{ + /// # client_error::ClientError, + /// # rpc_client::RpcClient, + /// # rpc_config::RpcSimulateTransactionConfig, + /// # }; + /// # use solana_sdk::{ + /// # commitment_config::CommitmentConfig, + /// # signature::Signature, + /// # signer::keypair::Keypair, + /// # hash::Hash, + /// # system_transaction, + /// # }; + /// # let rpc_client = RpcClient::new_mock("succeeds".to_string()); + /// // Transfer lamports from some account to a random account + /// let key = Keypair::new(); + /// let to = solana_sdk::pubkey::new_rand(); + /// let lamports = 50; + /// # let recent_blockhash = Hash::default(); + /// let tx = system_transaction::transfer(&key, &to, lamports, recent_blockhash); + /// let signature = rpc_client.send_transaction(&tx)?; + /// let commitment_config = CommitmentConfig::confirmed(); + /// let confirmed = rpc_client.confirm_transaction_with_commitment( + /// &signature, + /// commitment_config, + /// )?; + /// assert!(confirmed.value); + /// # Ok::<(), ClientError>(()) + /// ``` pub fn confirm_transaction_with_commitment( &self, signature: &Signature, @@ -238,6 +535,31 @@ impl RpcClient { }) } + /// # Examples + /// + /// ``` + /// # use solana_client::{ + /// # client_error::ClientError, + /// # rpc_client::RpcClient, + /// # }; + /// # use solana_sdk::{ + /// # signature::Signature, + /// # signer::keypair::Keypair, + /// # hash::Hash, + /// # system_transaction, + /// # }; + /// # let rpc_client = RpcClient::new_mock("succeeds".to_string()); + /// // Transfer lamports from some account to a random account + /// let key = Keypair::new(); + /// let to = solana_sdk::pubkey::new_rand(); + /// let lamports = 50; + /// # let recent_blockhash = Hash::default(); + /// let tx = system_transaction::transfer(&key, &to, lamports, recent_blockhash); + /// let signature = rpc_client.send_transaction(&tx)?; + /// let confirmed = rpc_client.confirm_transaction(&signature)?; + /// assert!(confirmed); + /// # Ok::<(), ClientError>(()) + /// ``` pub fn send_transaction(&self, transaction: &Transaction) -> ClientResult { self.send_transaction_with_config( transaction, @@ -258,6 +580,39 @@ impl RpcClient { } } + /// # Examples + /// + /// ``` + /// # use solana_client::{ + /// # client_error::ClientError, + /// # rpc_client::RpcClient, + /// # rpc_config::RpcSendTransactionConfig, + /// # }; + /// # use solana_sdk::{ + /// # signature::Signature, + /// # signer::keypair::Keypair, + /// # hash::Hash, + /// # system_transaction, + /// # }; + /// # let rpc_client = RpcClient::new_mock("succeeds".to_string()); + /// // Transfer lamports from some account to a random account + /// let key = Keypair::new(); + /// let to = solana_sdk::pubkey::new_rand(); + /// let lamports = 50; + /// # let recent_blockhash = Hash::default(); + /// let tx = system_transaction::transfer(&key, &to, lamports, recent_blockhash); + /// let config = RpcSendTransactionConfig { + /// skip_preflight: true, + /// .. RpcSendTransactionConfig::default() + /// }; + /// let signature = rpc_client.send_transaction_with_config( + /// &tx, + /// config, + /// )?; + /// let confirmed = rpc_client.confirm_transaction(&signature)?; + /// assert!(confirmed); + /// # Ok::<(), ClientError>(()) + /// ``` pub fn send_transaction_with_config( &self, transaction: &Transaction, @@ -325,6 +680,31 @@ impl RpcClient { } } + /// # Examples + /// + /// ``` + /// # use solana_client::{ + /// # client_error::ClientError, + /// # rpc_client::RpcClient, + /// # rpc_response::RpcSimulateTransactionResult, + /// # }; + /// # use solana_sdk::{ + /// # signature::Signature, + /// # signer::keypair::Keypair, + /// # hash::Hash, + /// # system_transaction, + /// # }; + /// # let rpc_client = RpcClient::new_mock("succeeds".to_string()); + /// // Transfer lamports from some account to a random account + /// let key = Keypair::new(); + /// let to = solana_sdk::pubkey::new_rand(); + /// let lamports = 50; + /// # let recent_blockhash = Hash::default(); + /// let tx = system_transaction::transfer(&key, &to, lamports, recent_blockhash); + /// let result = rpc_client.simulate_transaction(&tx)?; + /// assert!(result.value.err.is_none()); + /// # Ok::<(), ClientError>(()) + /// ``` pub fn simulate_transaction( &self, transaction: &Transaction, @@ -338,6 +718,39 @@ impl RpcClient { ) } + /// # Examples + /// + /// ``` + /// # use solana_client::{ + /// # client_error::ClientError, + /// # rpc_client::RpcClient, + /// # rpc_config::RpcSimulateTransactionConfig, + /// # rpc_response::RpcSimulateTransactionResult, + /// # }; + /// # use solana_sdk::{ + /// # signature::Signature, + /// # signer::keypair::Keypair, + /// # hash::Hash, + /// # system_transaction, + /// # }; + /// # let rpc_client = RpcClient::new_mock("succeeds".to_string()); + /// // Transfer lamports from some account to a random account + /// let key = Keypair::new(); + /// let to = solana_sdk::pubkey::new_rand(); + /// let lamports = 50; + /// # let recent_blockhash = Hash::default(); + /// let tx = system_transaction::transfer(&key, &to, lamports, recent_blockhash); + /// let config = RpcSimulateTransactionConfig { + /// sig_verify: false, + /// .. RpcSimulateTransactionConfig::default() + /// }; + /// let result = rpc_client.simulate_transaction_with_config( + /// &tx, + /// config, + /// )?; + /// assert!(result.value.err.is_none()); + /// # Ok::<(), ClientError>(()) + /// ``` pub fn simulate_transaction_with_config( &self, transaction: &Transaction, diff --git a/client/src/rpc_sender.rs b/client/src/rpc_sender.rs index 6574637b0a..75e5aab0e0 100644 --- a/client/src/rpc_sender.rs +++ b/client/src/rpc_sender.rs @@ -1,5 +1,18 @@ +//! A transport for RPC calls. + use crate::{client_error::Result, rpc_request::RpcRequest}; +/// A transport for RPC calls. +/// +/// `RpcSender` implements the underlying transport of requests to, and +/// responses from, a Solana node, and is used primarily by [`RpcClient`]. +/// +/// It is typically implemented by [`HttpSender`] in production, and +/// [`MockSender`] in unit tests. +/// +/// [`RpcClient`]: crate::rpc_client::RpcClient +/// [`HttpSender`]: crate::http_sender::HttpSender +/// [`MockSender`]: crate::mock_sender::MockSender pub trait RpcSender { fn send(&self, request: RpcRequest, params: serde_json::Value) -> Result; }