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