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