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
This commit is contained in:
Brian Anderson 2021-07-20 13:49:32 -05:00 committed by GitHub
parent 8549c19f1a
commit 5dcfd7ce74
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 474 additions and 1 deletions

View File

@ -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

View File

@ -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();

View File

@ -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<dyn RpcSender + Send + Sync + 'static>,
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<T: RpcSender + Send + Sync + 'static>(
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<bool> {
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<Signature> {
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,

View File

@ -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<serde_json::Value>;
}