diff --git a/client/src/nonblocking/blockhash_query.rs b/client/src/nonblocking/blockhash_query.rs new file mode 100644 index 0000000000..28943ad6a8 --- /dev/null +++ b/client/src/nonblocking/blockhash_query.rs @@ -0,0 +1,433 @@ +use { + crate::nonblocking::{nonce_utils, rpc_client::RpcClient}, + clap::ArgMatches, + solana_clap_utils::{ + input_parsers::{pubkey_of, value_of}, + nonce::*, + offline::*, + }, + solana_sdk::{commitment_config::CommitmentConfig, hash::Hash, pubkey::Pubkey}, +}; + +#[derive(Debug, PartialEq, Eq)] +pub enum Source { + Cluster, + NonceAccount(Pubkey), +} + +impl Source { + pub async fn get_blockhash( + &self, + rpc_client: &RpcClient, + commitment: CommitmentConfig, + ) -> Result> { + match self { + Self::Cluster => { + let (blockhash, _) = rpc_client + .get_latest_blockhash_with_commitment(commitment) + .await?; + Ok(blockhash) + } + Self::NonceAccount(ref pubkey) => { + #[allow(clippy::redundant_closure)] + let data = nonce_utils::get_account_with_commitment(rpc_client, pubkey, commitment) + .await + .and_then(|ref a| nonce_utils::data_from_account(a))?; + Ok(data.blockhash()) + } + } + } + + pub async fn is_blockhash_valid( + &self, + rpc_client: &RpcClient, + blockhash: &Hash, + commitment: CommitmentConfig, + ) -> Result> { + Ok(match self { + Self::Cluster => rpc_client.is_blockhash_valid(blockhash, commitment).await?, + Self::NonceAccount(ref pubkey) => { + #[allow(clippy::redundant_closure)] + let _ = nonce_utils::get_account_with_commitment(rpc_client, pubkey, commitment) + .await + .and_then(|ref a| nonce_utils::data_from_account(a))?; + true + } + }) + } +} + +#[derive(Debug, PartialEq, Eq)] +pub enum BlockhashQuery { + Static(Hash), + Validated(Source, Hash), + Rpc(Source), +} + +impl BlockhashQuery { + pub fn new(blockhash: Option, sign_only: bool, nonce_account: Option) -> Self { + let source = nonce_account + .map(Source::NonceAccount) + .unwrap_or(Source::Cluster); + match blockhash { + Some(hash) if sign_only => Self::Static(hash), + Some(hash) if !sign_only => Self::Validated(source, hash), + None if !sign_only => Self::Rpc(source), + _ => panic!("Cannot resolve blockhash"), + } + } + + pub fn new_from_matches(matches: &ArgMatches<'_>) -> Self { + let blockhash = value_of(matches, BLOCKHASH_ARG.name); + let sign_only = matches.is_present(SIGN_ONLY_ARG.name); + let nonce_account = pubkey_of(matches, NONCE_ARG.name); + BlockhashQuery::new(blockhash, sign_only, nonce_account) + } + + pub async fn get_blockhash( + &self, + rpc_client: &RpcClient, + commitment: CommitmentConfig, + ) -> Result> { + match self { + BlockhashQuery::Static(hash) => Ok(*hash), + BlockhashQuery::Validated(source, hash) => { + if !source + .is_blockhash_valid(rpc_client, hash, commitment) + .await? + { + return Err(format!("Hash has expired {:?}", hash).into()); + } + Ok(*hash) + } + BlockhashQuery::Rpc(source) => source.get_blockhash(rpc_client, commitment).await, + } + } +} + +impl Default for BlockhashQuery { + fn default() -> Self { + BlockhashQuery::Rpc(Source::Cluster) + } +} + +#[cfg(test)] +mod tests { + use { + super::*, + crate::{ + nonblocking::blockhash_query, + rpc_request::RpcRequest, + rpc_response::{Response, RpcBlockhash, RpcResponseContext}, + }, + clap::App, + serde_json::{self, json}, + solana_account_decoder::{UiAccount, UiAccountEncoding}, + solana_sdk::{ + account::Account, + fee_calculator::FeeCalculator, + hash::hash, + nonce::{self, state::DurableNonce}, + system_program, + }, + std::collections::HashMap, + }; + + #[test] + fn test_blockhash_query_new_ok() { + let blockhash = hash(&[1u8]); + let nonce_pubkey = Pubkey::new(&[1u8; 32]); + + assert_eq!( + BlockhashQuery::new(Some(blockhash), true, None), + BlockhashQuery::Static(blockhash), + ); + assert_eq!( + BlockhashQuery::new(Some(blockhash), false, None), + BlockhashQuery::Validated(blockhash_query::Source::Cluster, blockhash), + ); + assert_eq!( + BlockhashQuery::new(None, false, None), + BlockhashQuery::Rpc(blockhash_query::Source::Cluster) + ); + + assert_eq!( + BlockhashQuery::new(Some(blockhash), true, Some(nonce_pubkey)), + BlockhashQuery::Static(blockhash), + ); + assert_eq!( + BlockhashQuery::new(Some(blockhash), false, Some(nonce_pubkey)), + BlockhashQuery::Validated( + blockhash_query::Source::NonceAccount(nonce_pubkey), + blockhash + ), + ); + assert_eq!( + BlockhashQuery::new(None, false, Some(nonce_pubkey)), + BlockhashQuery::Rpc(blockhash_query::Source::NonceAccount(nonce_pubkey)), + ); + } + + #[test] + #[should_panic] + fn test_blockhash_query_new_no_nonce_fail() { + BlockhashQuery::new(None, true, None); + } + + #[test] + #[should_panic] + fn test_blockhash_query_new_nonce_fail() { + let nonce_pubkey = Pubkey::new(&[1u8; 32]); + BlockhashQuery::new(None, true, Some(nonce_pubkey)); + } + + #[test] + fn test_blockhash_query_new_from_matches_ok() { + let test_commands = App::new("blockhash_query_test") + .nonce_args(false) + .offline_args(); + let blockhash = hash(&[1u8]); + let blockhash_string = blockhash.to_string(); + + let matches = test_commands.clone().get_matches_from(vec![ + "blockhash_query_test", + "--blockhash", + &blockhash_string, + "--sign-only", + ]); + assert_eq!( + BlockhashQuery::new_from_matches(&matches), + BlockhashQuery::Static(blockhash), + ); + + let matches = test_commands.clone().get_matches_from(vec![ + "blockhash_query_test", + "--blockhash", + &blockhash_string, + ]); + assert_eq!( + BlockhashQuery::new_from_matches(&matches), + BlockhashQuery::Validated(blockhash_query::Source::Cluster, blockhash), + ); + + let matches = test_commands + .clone() + .get_matches_from(vec!["blockhash_query_test"]); + assert_eq!( + BlockhashQuery::new_from_matches(&matches), + BlockhashQuery::Rpc(blockhash_query::Source::Cluster), + ); + + let nonce_pubkey = Pubkey::new(&[1u8; 32]); + let nonce_string = nonce_pubkey.to_string(); + let matches = test_commands.clone().get_matches_from(vec![ + "blockhash_query_test", + "--blockhash", + &blockhash_string, + "--sign-only", + "--nonce", + &nonce_string, + ]); + assert_eq!( + BlockhashQuery::new_from_matches(&matches), + BlockhashQuery::Static(blockhash), + ); + + let matches = test_commands.clone().get_matches_from(vec![ + "blockhash_query_test", + "--blockhash", + &blockhash_string, + "--nonce", + &nonce_string, + ]); + assert_eq!( + BlockhashQuery::new_from_matches(&matches), + BlockhashQuery::Validated( + blockhash_query::Source::NonceAccount(nonce_pubkey), + blockhash + ), + ); + } + + #[test] + #[should_panic] + fn test_blockhash_query_new_from_matches_without_nonce_fail() { + let test_commands = App::new("blockhash_query_test") + .arg(blockhash_arg()) + // We can really only hit this case if the arg requirements + // are broken, so unset the requires() to recreate that condition + .arg(sign_only_arg().requires("")); + + let matches = test_commands + .clone() + .get_matches_from(vec!["blockhash_query_test", "--sign-only"]); + BlockhashQuery::new_from_matches(&matches); + } + + #[test] + #[should_panic] + fn test_blockhash_query_new_from_matches_with_nonce_fail() { + let test_commands = App::new("blockhash_query_test") + .arg(blockhash_arg()) + // We can really only hit this case if the arg requirements + // are broken, so unset the requires() to recreate that condition + .arg(sign_only_arg().requires("")); + let nonce_pubkey = Pubkey::new(&[1u8; 32]); + let nonce_string = nonce_pubkey.to_string(); + + let matches = test_commands.clone().get_matches_from(vec![ + "blockhash_query_test", + "--sign-only", + "--nonce", + &nonce_string, + ]); + BlockhashQuery::new_from_matches(&matches); + } + + #[tokio::test] + async fn test_blockhash_query_get_blockhash() { + let test_blockhash = hash(&[0u8]); + let rpc_blockhash = hash(&[1u8]); + + let get_latest_blockhash_response = json!(Response { + context: RpcResponseContext { + slot: 1, + api_version: None + }, + value: json!(RpcBlockhash { + blockhash: rpc_blockhash.to_string(), + last_valid_block_height: 42, + }), + }); + + let is_blockhash_valid_response = json!(Response { + context: RpcResponseContext { + slot: 1, + api_version: None + }, + value: true + }); + + let mut mocks = HashMap::new(); + mocks.insert( + RpcRequest::GetLatestBlockhash, + get_latest_blockhash_response.clone(), + ); + let rpc_client = RpcClient::new_mock_with_mocks("".to_string(), mocks); + assert_eq!( + BlockhashQuery::default() + .get_blockhash(&rpc_client, CommitmentConfig::default()) + .await + .unwrap(), + rpc_blockhash, + ); + + let mut mocks = HashMap::new(); + mocks.insert( + RpcRequest::GetLatestBlockhash, + get_latest_blockhash_response.clone(), + ); + mocks.insert( + RpcRequest::IsBlockhashValid, + is_blockhash_valid_response.clone(), + ); + let rpc_client = RpcClient::new_mock_with_mocks("".to_string(), mocks); + assert_eq!( + BlockhashQuery::Validated(Source::Cluster, test_blockhash) + .get_blockhash(&rpc_client, CommitmentConfig::default()) + .await + .unwrap(), + test_blockhash, + ); + + let mut mocks = HashMap::new(); + mocks.insert( + RpcRequest::GetLatestBlockhash, + get_latest_blockhash_response.clone(), + ); + let rpc_client = RpcClient::new_mock_with_mocks("".to_string(), mocks); + assert_eq!( + BlockhashQuery::Static(test_blockhash) + .get_blockhash(&rpc_client, CommitmentConfig::default()) + .await + .unwrap(), + test_blockhash, + ); + + let rpc_client = RpcClient::new_mock("fails".to_string()); + assert!(BlockhashQuery::default() + .get_blockhash(&rpc_client, CommitmentConfig::default()) + .await + .is_err()); + + let durable_nonce = DurableNonce::from_blockhash(&Hash::new(&[2u8; 32])); + let nonce_blockhash = *durable_nonce.as_hash(); + let nonce_fee_calc = FeeCalculator::new(4242); + let data = nonce::state::Data { + authority: Pubkey::new(&[3u8; 32]), + durable_nonce, + fee_calculator: nonce_fee_calc, + }; + let nonce_account = Account::new_data_with_space( + 42, + &nonce::state::Versions::new(nonce::State::Initialized(data)), + nonce::State::size(), + &system_program::id(), + ) + .unwrap(); + let nonce_pubkey = Pubkey::new(&[4u8; 32]); + let rpc_nonce_account = UiAccount::encode( + &nonce_pubkey, + &nonce_account, + UiAccountEncoding::Base64, + None, + None, + ); + let get_account_response = json!(Response { + context: RpcResponseContext { + slot: 1, + api_version: None + }, + value: json!(Some(rpc_nonce_account)), + }); + + let mut mocks = HashMap::new(); + mocks.insert(RpcRequest::GetAccountInfo, get_account_response.clone()); + let rpc_client = RpcClient::new_mock_with_mocks("".to_string(), mocks); + assert_eq!( + BlockhashQuery::Rpc(Source::NonceAccount(nonce_pubkey)) + .get_blockhash(&rpc_client, CommitmentConfig::default()) + .await + .unwrap(), + nonce_blockhash, + ); + + let mut mocks = HashMap::new(); + mocks.insert(RpcRequest::GetAccountInfo, get_account_response.clone()); + let rpc_client = RpcClient::new_mock_with_mocks("".to_string(), mocks); + assert_eq!( + BlockhashQuery::Validated(Source::NonceAccount(nonce_pubkey), nonce_blockhash) + .get_blockhash(&rpc_client, CommitmentConfig::default()) + .await + .unwrap(), + nonce_blockhash, + ); + + let mut mocks = HashMap::new(); + mocks.insert(RpcRequest::GetAccountInfo, get_account_response); + let rpc_client = RpcClient::new_mock_with_mocks("".to_string(), mocks); + assert_eq!( + BlockhashQuery::Static(nonce_blockhash) + .get_blockhash(&rpc_client, CommitmentConfig::default()) + .await + .unwrap(), + nonce_blockhash, + ); + + let rpc_client = RpcClient::new_mock("fails".to_string()); + assert!(BlockhashQuery::Rpc(Source::NonceAccount(nonce_pubkey)) + .get_blockhash(&rpc_client, CommitmentConfig::default()) + .await + .is_err()); + } +} diff --git a/client/src/nonblocking/mod.rs b/client/src/nonblocking/mod.rs index 844811c356..7ba96d3994 100644 --- a/client/src/nonblocking/mod.rs +++ b/client/src/nonblocking/mod.rs @@ -1,3 +1,5 @@ +pub mod blockhash_query; +pub mod nonce_utils; pub mod pubsub_client; pub mod quic_client; pub mod rpc_client; diff --git a/client/src/nonblocking/nonce_utils.rs b/client/src/nonblocking/nonce_utils.rs new file mode 100644 index 0000000000..fe0d2216d5 --- /dev/null +++ b/client/src/nonblocking/nonce_utils.rs @@ -0,0 +1,247 @@ +//! Durable transaction nonce helpers. + +use { + crate::nonblocking::rpc_client::RpcClient, + solana_sdk::{ + account::{Account, ReadableAccount}, + account_utils::StateMut, + commitment_config::CommitmentConfig, + hash::Hash, + nonce::{ + state::{Data, Versions}, + State, + }, + pubkey::Pubkey, + system_program, + }, +}; + +#[derive(Debug, thiserror::Error, PartialEq, Eq)] +pub enum Error { + #[error("invalid account owner")] + InvalidAccountOwner, + #[error("invalid account data")] + InvalidAccountData, + #[error("unexpected account data size")] + UnexpectedDataSize, + #[error("provided hash ({provided}) does not match nonce hash ({expected})")] + InvalidHash { provided: Hash, expected: Hash }, + #[error("provided authority ({provided}) does not match nonce authority ({expected})")] + InvalidAuthority { provided: Pubkey, expected: Pubkey }, + #[error("invalid state for requested operation")] + InvalidStateForOperation, + #[error("client error: {0}")] + 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 async fn get_account(rpc_client: &RpcClient, nonce_pubkey: &Pubkey) -> Result { + get_account_with_commitment(rpc_client, nonce_pubkey, CommitmentConfig::default()).await +} + +/// 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 async fn get_account_with_commitment( + rpc_client: &RpcClient, + nonce_pubkey: &Pubkey, + commitment: CommitmentConfig, +) -> Result { + rpc_client + .get_account_with_commitment(nonce_pubkey, commitment) + .await + .map_err(|e| Error::Client(format!("{}", e))) + .and_then(|result| { + result + .value + .ok_or_else(|| Error::Client(format!("AccountNotFound: pubkey={}", nonce_pubkey))) + }) + .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(account: &T) -> Result<(), Error> { + if account.owner() != &system_program::id() { + Err(Error::InvalidAccountOwner) + } else if account.data().is_empty() { + Err(Error::UnexpectedDataSize) + } else { + Ok(()) + } +} + +/// 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::nonblocking::{ +/// rpc_client::RpcClient, +/// nonce_utils, +/// }; +/// use solana_sdk::{ +/// nonce::State, +/// pubkey::Pubkey, +/// }; +/// use anyhow::Result; +/// +/// futures::executor::block_on(async { +/// async fn is_nonce_initialized( +/// client: &RpcClient, +/// nonce_account_pubkey: &Pubkey, +/// ) -> Result { +/// +/// // Sign the tx with nonce_account's `blockhash` instead of the +/// // network's latest blockhash. +/// let nonce_account = client.get_account(nonce_account_pubkey).await?; +/// 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).await?; +/// # Ok::<(), anyhow::Error>(()) +/// # })?; +/// # Ok::<(), anyhow::Error>(()) +/// ``` +pub fn state_from_account>( + account: &T, +) -> Result { + account_identity_ok(account)?; + let versions = StateMut::::state(account).map_err(|_| Error::InvalidAccountData)?; + Ok(State::from(versions)) +} + +/// 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::nonblocking::{ +/// 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; +/// +/// futures::executor::block_on(async { +/// async 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).await?; +/// 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")).await?; +/// # +/// # Ok::<(), anyhow::Error>(()) +/// # })?; +/// # Ok::<(), anyhow::Error>(()) +/// ``` +pub fn data_from_account>( + account: &T, +) -> Result { + 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), + State::Initialized(data) => Ok(data), + } +} diff --git a/client/src/nonce_utils.rs b/client/src/nonce_utils.rs index 9d79777155..b00ef416c5 100644 --- a/client/src/nonce_utils.rs +++ b/client/src/nonce_utils.rs @@ -1,39 +1,13 @@ //! Durable transaction nonce helpers. +pub use crate::nonblocking::nonce_utils::{ + account_identity_ok, data_from_account, data_from_state, state_from_account, Error, +}; use { crate::rpc_client::RpcClient, - solana_sdk::{ - account::{Account, ReadableAccount}, - account_utils::StateMut, - commitment_config::CommitmentConfig, - hash::Hash, - nonce::{ - state::{Data, Versions}, - State, - }, - pubkey::Pubkey, - system_program, - }, + solana_sdk::{account::Account, commitment_config::CommitmentConfig, pubkey::Pubkey}, }; -#[derive(Debug, thiserror::Error, PartialEq, Eq)] -pub enum Error { - #[error("invalid account owner")] - InvalidAccountOwner, - #[error("invalid account data")] - InvalidAccountData, - #[error("unexpected account data size")] - UnexpectedDataSize, - #[error("provided hash ({provided}) does not match nonce hash ({expected})")] - InvalidHash { provided: Hash, expected: Hash }, - #[error("provided authority ({provided}) does not match nonce authority ({expected})")] - InvalidAuthority { provided: Pubkey, expected: Pubkey }, - #[error("invalid state for requested operation")] - InvalidStateForOperation, - #[error("client error: {0}")] - Client(String), -} - /// Get a nonce account from the network. /// /// This is like [`RpcClient::get_account`] except: @@ -66,176 +40,3 @@ 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(account: &T) -> Result<(), Error> { - if account.owner() != &system_program::id() { - Err(Error::InvalidAccountOwner) - } else if account.data().is_empty() { - Err(Error::UnexpectedDataSize) - } else { - Ok(()) - } -} - -/// 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 { -/// -/// // 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>( - account: &T, -) -> Result { - account_identity_ok(account)?; - let versions = StateMut::::state(account).map_err(|_| Error::InvalidAccountData)?; - Ok(State::from(versions)) -} - -/// 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>( - account: &T, -) -> Result { - 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), - State::Initialized(data) => Ok(data), - } -}