solana_rpc_client_nonce_utils/nonblocking/mod.rs
1//! Durable transaction nonce helpers.
2
3pub mod blockhash_query;
4
5use {
6 solana_rpc_client::nonblocking::rpc_client::RpcClient,
7 solana_sdk::{
8 account::{Account, ReadableAccount},
9 account_utils::StateMut,
10 commitment_config::CommitmentConfig,
11 hash::Hash,
12 nonce::{
13 state::{Data, Versions},
14 State,
15 },
16 pubkey::Pubkey,
17 system_program,
18 },
19};
20
21#[derive(Debug, thiserror::Error, PartialEq, Eq)]
22pub enum Error {
23 #[error("invalid account owner")]
24 InvalidAccountOwner,
25 #[error("invalid account data")]
26 InvalidAccountData,
27 #[error("unexpected account data size")]
28 UnexpectedDataSize,
29 #[error("provided hash ({provided}) does not match nonce hash ({expected})")]
30 InvalidHash { provided: Hash, expected: Hash },
31 #[error("provided authority ({provided}) does not match nonce authority ({expected})")]
32 InvalidAuthority { provided: Pubkey, expected: Pubkey },
33 #[error("invalid state for requested operation")]
34 InvalidStateForOperation,
35 #[error("client error: {0}")]
36 Client(String),
37}
38
39/// Get a nonce account from the network.
40///
41/// This is like [`RpcClient::get_account`] except:
42///
43/// - it returns this module's [`Error`] type,
44/// - it returns an error if any of the checks from [`account_identity_ok`] fail.
45pub async fn get_account(rpc_client: &RpcClient, nonce_pubkey: &Pubkey) -> Result<Account, Error> {
46 get_account_with_commitment(rpc_client, nonce_pubkey, CommitmentConfig::default()).await
47}
48
49/// Get a nonce account from the network.
50///
51/// This is like [`RpcClient::get_account_with_commitment`] except:
52///
53/// - it returns this module's [`Error`] type,
54/// - it returns an error if the account does not exist,
55/// - it returns an error if any of the checks from [`account_identity_ok`] fail.
56pub async fn get_account_with_commitment(
57 rpc_client: &RpcClient,
58 nonce_pubkey: &Pubkey,
59 commitment: CommitmentConfig,
60) -> Result<Account, Error> {
61 rpc_client
62 .get_account_with_commitment(nonce_pubkey, commitment)
63 .await
64 .map_err(|e| Error::Client(format!("{e}")))
65 .and_then(|result| {
66 result
67 .value
68 .ok_or_else(|| Error::Client(format!("AccountNotFound: pubkey={nonce_pubkey}")))
69 })
70 .and_then(|a| account_identity_ok(&a).map(|()| a))
71}
72
73/// Perform basic checks that an account has nonce-like properties.
74///
75/// # Errors
76///
77/// Returns [`Error::InvalidAccountOwner`] if the account is not owned by the
78/// system program. Returns [`Error::UnexpectedDataSize`] if the account
79/// contains no data.
80pub fn account_identity_ok<T: ReadableAccount>(account: &T) -> Result<(), Error> {
81 if account.owner() != &system_program::id() {
82 Err(Error::InvalidAccountOwner)
83 } else if account.data().is_empty() {
84 Err(Error::UnexpectedDataSize)
85 } else {
86 Ok(())
87 }
88}
89
90/// Deserialize the state of a durable transaction nonce account.
91///
92/// # Errors
93///
94/// Returns an error if the account is not owned by the system program or
95/// contains no data.
96///
97/// # Examples
98///
99/// Determine if a nonce account is initialized:
100///
101/// ```no_run
102/// use solana_rpc_client_nonce_utils::nonblocking;
103/// use solana_rpc_client::nonblocking::rpc_client::RpcClient;
104/// use solana_sdk::{
105/// nonce::State,
106/// pubkey::Pubkey,
107/// };
108/// use anyhow::Result;
109///
110/// futures::executor::block_on(async {
111/// async fn is_nonce_initialized(
112/// client: &RpcClient,
113/// nonce_account_pubkey: &Pubkey,
114/// ) -> Result<bool> {
115///
116/// // Sign the tx with nonce_account's `blockhash` instead of the
117/// // network's latest blockhash.
118/// let nonce_account = client.get_account(nonce_account_pubkey).await?;
119/// let nonce_state = nonblocking::state_from_account(&nonce_account)?;
120///
121/// Ok(!matches!(nonce_state, State::Uninitialized))
122/// }
123/// #
124/// # let client = RpcClient::new(String::new());
125/// # let nonce_account_pubkey = Pubkey::new_unique();
126/// # is_nonce_initialized(&client, &nonce_account_pubkey).await?;
127/// # Ok::<(), anyhow::Error>(())
128/// # })?;
129/// # Ok::<(), anyhow::Error>(())
130/// ```
131pub fn state_from_account<T: ReadableAccount + StateMut<Versions>>(
132 account: &T,
133) -> Result<State, Error> {
134 account_identity_ok(account)?;
135 let versions = StateMut::<Versions>::state(account).map_err(|_| Error::InvalidAccountData)?;
136 Ok(State::from(versions))
137}
138
139/// Deserialize the state data of a durable transaction nonce account.
140///
141/// # Errors
142///
143/// Returns an error if the account is not owned by the system program or
144/// contains no data. Returns an error if the account state is uninitialized or
145/// fails to deserialize.
146///
147/// # Examples
148///
149/// Create and sign a transaction with a durable nonce:
150///
151/// ```no_run
152/// use solana_rpc_client_nonce_utils::nonblocking;
153/// use solana_rpc_client::nonblocking::rpc_client::RpcClient;
154/// use solana_sdk::{
155/// message::Message,
156/// pubkey::Pubkey,
157/// signature::{Keypair, Signer},
158/// system_instruction,
159/// transaction::Transaction,
160/// };
161/// use std::path::Path;
162/// use anyhow::Result;
163/// # use anyhow::anyhow;
164///
165/// futures::executor::block_on(async {
166/// async fn create_transfer_tx_with_nonce(
167/// client: &RpcClient,
168/// nonce_account_pubkey: &Pubkey,
169/// payer: &Keypair,
170/// receiver: &Pubkey,
171/// amount: u64,
172/// tx_path: &Path,
173/// ) -> Result<()> {
174///
175/// let instr_transfer = system_instruction::transfer(
176/// &payer.pubkey(),
177/// receiver,
178/// amount,
179/// );
180///
181/// // In this example, `payer` is `nonce_account_pubkey`'s authority
182/// let instr_advance_nonce_account = system_instruction::advance_nonce_account(
183/// nonce_account_pubkey,
184/// &payer.pubkey(),
185/// );
186///
187/// // The `advance_nonce_account` instruction must be the first issued in
188/// // the transaction.
189/// let message = Message::new(
190/// &[
191/// instr_advance_nonce_account,
192/// instr_transfer
193/// ],
194/// Some(&payer.pubkey()),
195/// );
196///
197/// let mut tx = Transaction::new_unsigned(message);
198///
199/// // Sign the tx with nonce_account's `blockhash` instead of the
200/// // network's latest blockhash.
201/// let nonce_account = client.get_account(nonce_account_pubkey).await?;
202/// let nonce_data = nonblocking::data_from_account(&nonce_account)?;
203/// let blockhash = nonce_data.blockhash();
204///
205/// tx.try_sign(&[payer], blockhash)?;
206///
207/// // Save the signed transaction locally for later submission.
208/// save_tx_to_file(&tx_path, &tx)?;
209///
210/// Ok(())
211/// }
212/// #
213/// # fn save_tx_to_file(path: &Path, tx: &Transaction) -> Result<()> {
214/// # Ok(())
215/// # }
216/// #
217/// # let client = RpcClient::new(String::new());
218/// # let nonce_account_pubkey = Pubkey::new_unique();
219/// # let payer = Keypair::new();
220/// # let receiver = Pubkey::new_unique();
221/// # create_transfer_tx_with_nonce(&client, &nonce_account_pubkey, &payer, &receiver, 1024, Path::new("new_tx")).await?;
222/// #
223/// # Ok::<(), anyhow::Error>(())
224/// # })?;
225/// # Ok::<(), anyhow::Error>(())
226/// ```
227pub fn data_from_account<T: ReadableAccount + StateMut<Versions>>(
228 account: &T,
229) -> Result<Data, Error> {
230 account_identity_ok(account)?;
231 state_from_account(account).and_then(|ref s| data_from_state(s).cloned())
232}
233
234/// Get the nonce data from its [`State`] value.
235///
236/// # Errors
237///
238/// Returns [`Error::InvalidStateForOperation`] if `state` is
239/// [`State::Uninitialized`].
240pub fn data_from_state(state: &State) -> Result<&Data, Error> {
241 match state {
242 State::Uninitialized => Err(Error::InvalidStateForOperation),
243 State::Initialized(data) => Ok(data),
244 }
245}