Skip to content

Commit

Permalink
Document APIs related to durable transaction nonces
Browse files Browse the repository at this point in the history
  • Loading branch information
brson authored and t-nelson committed Mar 31, 2022
1 parent b741b86 commit 210d98b
Show file tree
Hide file tree
Showing 8 changed files with 608 additions and 7 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ tungstenite = { version = "0.17.2", features = ["rustls-tls-webpki-roots"] }
url = "2.2.2"

[dev-dependencies]
anyhow = "1.0.45"
assert_matches = "1.5.0"
jsonrpc-http-server = "18.0.0"
solana-logger = { path = "../logger", version = "=1.11.0" }
Expand Down
156 changes: 156 additions & 0 deletions client/src/nonce_utils.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//! Durable transaction nonce helpers.
use {
crate::rpc_client::RpcClient,
solana_sdk::{
Expand Down Expand Up @@ -32,10 +34,23 @@ pub enum Error {
Client(String),
}

/// Get a nonce account from the network.
///
/// This is like [`RpcClient::get_account`] except:
///
/// - it returns this module's [`Error`] type,
/// - it returns an error if any of the checks from [`account_identity_ok`] fail.
pub fn get_account(rpc_client: &RpcClient, nonce_pubkey: &Pubkey) -> Result<Account, Error> {
get_account_with_commitment(rpc_client, nonce_pubkey, CommitmentConfig::default())
}

/// Get a nonce account from the network.
///
/// This is like [`RpcClient::get_account_with_commitment`] except:
///
/// - it returns this module's [`Error`] type,
/// - it returns an error if the account does not exist,
/// - it returns an error if any of the checks from [`account_identity_ok`] fail.
pub fn get_account_with_commitment(
rpc_client: &RpcClient,
nonce_pubkey: &Pubkey,
Expand All @@ -52,6 +67,13 @@ pub fn get_account_with_commitment(
.and_then(|a| account_identity_ok(&a).map(|()| a))
}

/// Perform basic checks that an account has nonce-like properties.
///
/// # Errors
///
/// Returns [`Error::InvalidAccountOwner`] if the account is not owned by the
/// system program. Returns [`Error::UnexpectedDataSize`] if the account
/// contains no data.
pub fn account_identity_ok<T: ReadableAccount>(account: &T) -> Result<(), Error> {
if account.owner() != &system_program::id() {
Err(Error::InvalidAccountOwner)
Expand All @@ -62,6 +84,47 @@ pub fn account_identity_ok<T: ReadableAccount>(account: &T) -> Result<(), Error>
}
}

/// Deserialize the state of a durable transaction nonce account.
///
/// # Errors
///
/// Returns an error if the account is not owned by the system program or
/// contains no data.
///
/// # Examples
///
/// Determine if a nonce account is initialized:
///
/// ```no_run
/// use solana_client::{
/// rpc_client::RpcClient,
/// nonce_utils,
/// };
/// use solana_sdk::{
/// nonce::State,
/// pubkey::Pubkey,
/// };
/// use anyhow::Result;
///
/// fn is_nonce_initialized(
/// client: &RpcClient,
/// nonce_account_pubkey: &Pubkey,
/// ) -> Result<bool> {
///
/// // Sign the tx with nonce_account's `blockhash` instead of the
/// // network's latest blockhash.
/// let nonce_account = client.get_account(&nonce_account_pubkey)?;
/// let nonce_state = nonce_utils::state_from_account(&nonce_account)?;
///
/// Ok(!matches!(nonce_state, State::Uninitialized))
/// }
/// #
/// # let client = RpcClient::new(String::new());
/// # let nonce_account_pubkey = Pubkey::new_unique();
/// # is_nonce_initialized(&client, &nonce_account_pubkey)?;
/// #
/// # Ok::<(), anyhow::Error>(())
/// ```
pub fn state_from_account<T: ReadableAccount + StateMut<Versions>>(
account: &T,
) -> Result<State, Error> {
Expand All @@ -71,13 +134,106 @@ pub fn state_from_account<T: ReadableAccount + StateMut<Versions>>(
.map(|v| v.convert_to_current())
}

/// Deserialize the state data of a durable transaction nonce account.
///
/// # Errors
///
/// Returns an error if the account is not owned by the system program or
/// contains no data. Returns an error if the account state is uninitialized or
/// fails to deserialize.
///
/// # Examples
///
/// Create and sign a transaction with a durable nonce:
///
/// ```no_run
/// use solana_client::{
/// rpc_client::RpcClient,
/// nonce_utils,
/// };
/// use solana_sdk::{
/// message::Message,
/// pubkey::Pubkey,
/// signature::{Keypair, Signer},
/// system_instruction,
/// transaction::Transaction,
/// };
/// use std::path::Path;
/// use anyhow::Result;
/// # use anyhow::anyhow;
///
/// fn create_transfer_tx_with_nonce(
/// client: &RpcClient,
/// nonce_account_pubkey: &Pubkey,
/// payer: &Keypair,
/// receiver: &Pubkey,
/// amount: u64,
/// tx_path: &Path,
/// ) -> Result<()> {
///
/// let instr_transfer = system_instruction::transfer(
/// &payer.pubkey(),
/// receiver,
/// amount,
/// );
///
/// // In this example, `payer` is `nonce_account_pubkey`'s authority
/// let instr_advance_nonce_account = system_instruction::advance_nonce_account(
/// nonce_account_pubkey,
/// &payer.pubkey(),
/// );
///
/// // The `advance_nonce_account` instruction must be the first issued in
/// // the transaction.
/// let message = Message::new(
/// &[
/// instr_advance_nonce_account,
/// instr_transfer
/// ],
/// Some(&payer.pubkey()),
/// );
///
/// let mut tx = Transaction::new_unsigned(message);
///
/// // Sign the tx with nonce_account's `blockhash` instead of the
/// // network's latest blockhash.
/// let nonce_account = client.get_account(&nonce_account_pubkey)?;
/// let nonce_data = nonce_utils::data_from_account(&nonce_account)?;
/// let blockhash = nonce_data.blockhash;
///
/// tx.try_sign(&[payer], blockhash)?;
///
/// // Save the signed transaction locally for later submission.
/// save_tx_to_file(&tx_path, &tx)?;
///
/// Ok(())
/// }
/// #
/// # fn save_tx_to_file(path: &Path, tx: &Transaction) -> Result<()> {
/// # Ok(())
/// # }
/// #
/// # let client = RpcClient::new(String::new());
/// # let nonce_account_pubkey = Pubkey::new_unique();
/// # let payer = Keypair::new();
/// # let receiver = Pubkey::new_unique();
/// # create_transfer_tx_with_nonce(&client, &nonce_account_pubkey, &payer, &receiver, 1024, Path::new("new_tx"))?;
/// #
/// # Ok::<(), anyhow::Error>(())
/// ```
pub fn data_from_account<T: ReadableAccount + StateMut<Versions>>(
account: &T,
) -> Result<Data, Error> {
account_identity_ok(account)?;
state_from_account(account).and_then(|ref s| data_from_state(s).map(|d| d.clone()))
}

/// Get the nonce data from its [`State`] value.
///
/// # Errors
///
/// Returns [`Error::InvalidStateForOperation`] if `state` is
/// [`State::Uninitialized`].
pub fn data_from_state(state: &State) -> Result<&Data, Error> {
match state {
State::Uninitialized => Err(Error::InvalidStateForOperation),
Expand Down
81 changes: 74 additions & 7 deletions sdk/program/src/example_mocks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,38 @@

pub mod solana_client {
pub mod client_error {
use thiserror::Error;

#[derive(Error, Debug)]
#[derive(thiserror::Error, Debug)]
#[error("mock-error")]
pub struct ClientError;
pub type Result<T> = std::result::Result<T, ClientError>;
}

pub mod nonce_utils {
use {
super::super::solana_sdk::{
account::ReadableAccount, account_utils::StateMut, hash::Hash, pubkey::Pubkey,
},
crate::nonce::state::{Data, Versions},
};

#[derive(thiserror::Error, Debug)]
#[error("mock-error")]
pub struct Error;

pub fn data_from_account<T: ReadableAccount + StateMut<Versions>>(
_account: &T,
) -> Result<Data, Error> {
Ok(Data::new(Pubkey::new_unique(), Hash::default(), 5000))
}
}

pub mod rpc_client {
use super::{
super::solana_sdk::{hash::Hash, signature::Signature, transaction::Transaction},
client_error::Result as ClientResult,
super::solana_sdk::{
account::Account, hash::Hash, pubkey::Pubkey, signature::Signature,
transaction::Transaction,
},
client_error::{ClientError, Result as ClientResult},
};

pub struct RpcClient;
Expand All @@ -53,6 +73,14 @@ pub mod solana_client {
) -> ClientResult<u64> {
Ok(0)
}

pub fn get_account(&self, _pubkey: &Pubkey) -> Result<Account, ClientError> {
Ok(Account {})
}

pub fn get_balance(&self, _pubkey: &Pubkey) -> ClientResult<u64> {
Ok(0)
}
}
}
}
Expand All @@ -63,7 +91,33 @@ pub mod solana_client {
/// This lets examples in solana-program appear to be written as client
/// programs.
pub mod solana_sdk {
pub use crate::{hash, instruction, message, nonce, pubkey, system_instruction};
pub use crate::{
hash, instruction, message, nonce,
pubkey::{self, Pubkey},
system_instruction,
};

pub mod account {
pub struct Account;

pub trait ReadableAccount: Sized {
fn data(&self) -> &[u8];
}

impl ReadableAccount for Account {
fn data(&self) -> &[u8] {
&[0]
}
}
}

pub mod account_utils {
use super::account::Account;

pub trait StateMut<T> {}

impl<T> StateMut<T> for Account {}
}

pub mod signature {
use crate::pubkey::Pubkey;
Expand Down Expand Up @@ -93,6 +147,9 @@ pub mod solana_sdk {
pub mod signers {
use super::signature::Signer;

#[derive(Debug, thiserror::Error, PartialEq)]
pub enum SignerError {}

pub trait Signers {}

impl<T: Signer> Signers for [&T; 1] {}
Expand All @@ -101,10 +158,12 @@ pub mod solana_sdk {

pub mod transaction {
use {
super::signers::Signers,
super::signers::{SignerError, Signers},
crate::{hash::Hash, instruction::Instruction, message::Message, pubkey::Pubkey},
serde::Serialize,
};

#[derive(Serialize)]
pub struct Transaction {
pub message: Message,
}
Expand Down Expand Up @@ -143,6 +202,14 @@ pub mod solana_sdk {
}

pub fn sign<T: Signers>(&mut self, _keypairs: &T, _recent_blockhash: Hash) {}

pub fn try_sign<T: Signers>(
&mut self,
_keypairs: &T,
_recent_blockhash: Hash,
) -> Result<(), SignerError> {
Ok(())
}
}
}
}
2 changes: 2 additions & 0 deletions sdk/program/src/nonce/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//! Durable transaction nonces.
pub mod state;
pub use state::State;

Expand Down
Loading

0 comments on commit 210d98b

Please sign in to comment.