-
Notifications
You must be signed in to change notification settings - Fork 4.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement nonblocking version of BlockhashQuery
Showing
4 changed files
with
681 additions
and
202 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<Hash, Box<dyn std::error::Error>> { | ||
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<bool, Box<dyn std::error::Error>> { | ||
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<Hash>, sign_only: bool, nonce_account: Option<Pubkey>) -> 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<Hash, Box<dyn std::error::Error>> { | ||
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()); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,5 @@ | ||
pub mod blockhash_query; | ||
pub mod nonce_utils; | ||
pub mod pubsub_client; | ||
pub mod quic_client; | ||
pub mod rpc_client; | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,242 @@ | ||
//! 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<Account, Error> { | ||
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<Account, Error> { | ||
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<T: ReadableAccount>(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; | ||
/// | ||
/// 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).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)?; | ||
/// # | ||
/// # Ok::<(), anyhow::Error>(()) | ||
/// ``` | ||
pub fn state_from_account<T: ReadableAccount + StateMut<Versions>>( | ||
account: &T, | ||
) -> Result<State, Error> { | ||
account_identity_ok(account)?; | ||
let versions = StateMut::<Versions>::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; | ||
/// | ||
/// 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"))?; | ||
/// # | ||
/// # 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), | ||
State::Initialized(data) => Ok(data), | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters