From 8dd62854fa9acb85fe9dbb5b894944f4383e04fc Mon Sep 17 00:00:00 2001 From: Brian Anderson Date: Fri, 21 Jan 2022 19:30:12 -0600 Subject: [PATCH] Document transaction module (#22440) * Document transaction module * example_mocks is only for feature = full --- Cargo.lock | 1 + sdk/Cargo.toml | 1 + sdk/src/example_mocks.rs | 47 +++ sdk/src/lib.rs | 1 + sdk/src/transaction/mod.rs | 792 ++++++++++++++++++++++++++++++++++--- 5 files changed, 789 insertions(+), 53 deletions(-) create mode 100644 sdk/src/example_mocks.rs diff --git a/Cargo.lock b/Cargo.lock index e1547f49d6..2ee844ae69 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5659,6 +5659,7 @@ dependencies = [ name = "solana-sdk" version = "1.10.0" dependencies = [ + "anyhow", "assert_matches", "base64 0.13.0", "bincode", diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml index e77124b787..016dde34d8 100644 --- a/sdk/Cargo.toml +++ b/sdk/Cargo.toml @@ -84,6 +84,7 @@ wasm-bindgen = "0.2" js-sys = "0.3.55" [dev-dependencies] +anyhow = "1.0.45" curve25519-dalek = "3.2.0" tiny-bip39 = "0.8.2" diff --git a/sdk/src/example_mocks.rs b/sdk/src/example_mocks.rs new file mode 100644 index 0000000000..47106d9f16 --- /dev/null +++ b/sdk/src/example_mocks.rs @@ -0,0 +1,47 @@ +//! Mock types for use in examples. +//! +//! These represent APIs from crates that themselves depend on this crate, and +//! which are useful for illustrating the examples for APIs in this crate. +//! +//! Directly depending on these crates though would cause problematic circular +//! dependencies, so instead they are mocked out here in a way that allows +//! examples to appear to use crates that this crate must not depend on. +//! +//! Each mod here has the name of a crate, so that examples can be structured to +//! appear to import from that crate. + +#![doc(hidden)] +#![cfg(feature = "full")] + +pub mod solana_client { + pub mod client_error { + use thiserror::Error; + + #[derive(Error, Debug)] + #[error("mock-error")] + pub struct ClientError; + pub type Result = std::result::Result; + } + + pub mod rpc_client { + use super::client_error::Result as ClientResult; + use crate::{hash::Hash, signature::Signature, transaction::Transaction}; + + pub struct RpcClient; + + impl RpcClient { + pub fn new(_url: String) -> Self { + RpcClient + } + pub fn get_latest_blockhash(&self) -> ClientResult { + Ok(Hash::default()) + } + pub fn send_and_confirm_transaction( + &self, + _transaction: &Transaction, + ) -> ClientResult { + Ok(Signature::default()) + } + } + } +} diff --git a/sdk/src/lib.rs b/sdk/src/lib.rs index ded554312d..38cf07db86 100644 --- a/sdk/src/lib.rs +++ b/sdk/src/lib.rs @@ -21,6 +21,7 @@ pub mod ed25519_instruction; pub mod entrypoint; pub mod entrypoint_deprecated; pub mod epoch_info; +pub mod example_mocks; pub mod exit; pub mod feature; pub mod feature_set; diff --git a/sdk/src/transaction/mod.rs b/sdk/src/transaction/mod.rs index eba6a2c0d2..0232075487 100644 --- a/sdk/src/transaction/mod.rs +++ b/sdk/src/transaction/mod.rs @@ -1,4 +1,113 @@ -//! Defines a Transaction type to package an atomic sequence of instructions. +//! Atomically-committed sequences of instructions. +//! +//! While [`Instruction`]s are the basic unit of computation in Solana, they are +//! submitted by clients in [`Transaction`]s containing one or more +//! instructions, and signed by one or more [`Signer`]s. Solana executes the +//! instructions in a transaction in order, and only commits any changes if all +//! instructions terminate without producing an error or exception. +//! +//! Transactions do not directly contain their instructions but instead include +//! a [`Message`], a precompiled representation of a sequence of instructions. +//! `Message`'s constructors handle the complex task of reordering the +//! individual lists of accounts required by each instruction into a single flat +//! list of deduplicated accounts required by the Solana runtime. The +//! `Transaction` type has constructors that build the `Message` so that clients +//! don't need to interact with them directly. +//! +//! Prior to submission to the network, transactions must be signed by one or or +//! more keypairs, and this signing is typically performed by an abstract +//! [`Signer`], which may be a [`Keypair`] but may also be other types of +//! signers including remote wallets, such as Ledger devices, as represented by +//! the [`RemoteKeypair`] type in the [`solana-remote-wallet`] crate. +//! +//! [`Signer`]: crate::signer::Signer +//! [`Keypair`]: crate::signer::keypair::Keypair +//! [`solana-remote-wallet`]: https://docs.rs/solana-remote-wallet/latest/ +//! [`RemoteKeypair`]: https://docs.rs/solana-remote-wallet/latest/solana_remote_wallet/remote_keypair/struct.RemoteKeypair.html +//! +//! Every transaction must be signed by a fee-paying account, the account from +//! which the cost of executing the transaction is withdrawn. Other required +//! signatures are determined by the requirements of the programs being executed +//! by each instruction, and are conventionally specified by that program's +//! documentation. +//! +//! When signing a transaction, a recent blockhash must be provided (which can +//! be retrieved with [`RpcClient::get_latest_blockhash`]). This allows +//! validators to drop old but unexecuted transactions; and to distinguish +//! between accidentally duplicated transactions and intentionally duplicated +//! transactions — any identical transactions will not be executed more +//! than once, so updating the blockhash between submitting otherwise identical +//! transactions makes them unique. If a client must sign a transaction long +//! before submitting it to the network, then it can use the _[durable +//! transaction nonce]_ mechanism instead of a recent blockhash to ensure unique +//! transactions. +//! +//! [`RpcClient::get_latest_blockhash`]: https://docs.rs/solana-client/latest/solana_client/rpc_client/struct.RpcClient.html#method.get_latest_blockhash +//! [durable transaction nonce]: https://docs.solana.com/implemented-proposals/durable-tx-nonces +//! +//! # Examples +//! +//! This example uses the [`solana_client`] and [`anyhow`] crates. +//! +//! [`solana_client`]: https://docs.rs/solana-client +//! [`anyhow`]: https://docs.rs/anyhow +//! +//! ``` +//! # use solana_sdk::example_mocks::solana_client; +//! use anyhow::Result; +//! use borsh::{BorshSerialize, BorshDeserialize}; +//! use solana_client::rpc_client::RpcClient; +//! use solana_sdk::{ +//! instruction::Instruction, +//! message::Message, +//! pubkey::Pubkey, +//! signature::{Keypair, Signer}, +//! transaction::Transaction, +//! }; +//! +//! // A custom program instruction. This would typically be defined in +//! // another crate so it can be shared between the on-chain program and +//! // the client. +//! #[derive(BorshSerialize, BorshDeserialize)] +//! enum BankInstruction { +//! Initialize, +//! Deposit { lamports: u64 }, +//! Withdraw { lamports: u64 }, +//! } +//! +//! fn send_initialize_tx( +//! client: &RpcClient, +//! program_id: Pubkey, +//! payer: &Keypair +//! ) -> Result<()> { +//! +//! let bank_instruction = BankInstruction::Initialize; +//! +//! let instruction = Instruction::new_with_borsh( +//! program_id, +//! &bank_instruction, +//! vec![], +//! ); +//! +//! let blockhash = client.get_latest_blockhash()?; +//! let mut tx = Transaction::new_signed_with_payer( +//! &[instruction], +//! Some(&payer.pubkey()), +//! &[payer], +//! blockhash, +//! ); +//! client.send_and_confirm_transaction(&tx)?; +//! +//! Ok(()) +//! } +//! # +//! # let client = RpcClient::new(String::new()); +//! # let program_id = Pubkey::new_unique(); +//! # let payer = Keypair::new(); +//! # send_initialize_tx(&client, program_id, &payer)?; +//! # +//! # Ok::<(), anyhow::Error>(()) +//! ``` #![cfg(feature = "full")] @@ -38,16 +147,38 @@ pub enum TransactionVerificationMode { pub type Result = result::Result; -/// An atomic transaction +/// An atomically-commited sequence of instructions. +/// +/// While [`Instruction`]s are the basic unit of computation in Solana, +/// they are submitted by clients in [`Transaction`]s containing one or +/// more instructions, and signed by one or more [`Signer`]s. +/// +/// [`Signer`]: crate::signer::Signer +/// +/// See the [module documentation] for more details about transactions. +/// +/// [module documentation]: self +/// +/// Some constructors accept an optional `payer`, the account responsible for +/// paying the cost of executing a transaction. In most cases, callers should +/// specify the payer explicitly in these constructors. In some cases though, +/// the caller is not _required_ to specify the payer, but is still allowed to: +/// in the [`Message`] structure, the first account is always the fee-payer, so +/// if the caller has knowledge that the first account of the constructed +/// transaction's `Message` is both a signer and the expected fee-payer, then +/// redundantly specifying the fee-payer is not strictly required. #[wasm_bindgen] #[frozen_abi(digest = "FZtncnS1Xk8ghHfKiXE5oGiUbw2wJhmfXQuNgQR3K6Mc")] #[derive(Debug, PartialEq, Default, Eq, Clone, Serialize, Deserialize, AbiExample)] pub struct Transaction { - /// A set of digital signatures of a serialized [`Message`], signed by the - /// first `signatures.len()` keys of [`account_keys`]. + /// A set of signatures of a serialized [`Message`], signed by the first + /// keys of the `Message`'s [`account_keys`], where the number of signatures + /// is equal to [`num_required_signatures`] of the `Message`'s + /// [`MessageHeader`]. /// /// [`account_keys`]: Message::account_keys - /// + /// [`MessageHeader`]: crate::message::MessageHeader + /// [`num_required_signatures`]: crate::message::MessageHeader::num_required_signatures // NOTE: Serialization-related changes must be paired with the direct read at sigverify. #[wasm_bindgen(skip)] #[serde(with = "short_vec")] @@ -71,6 +202,72 @@ impl Sanitize for Transaction { } impl Transaction { + /// Create an unsigned transaction from a [`Message`]. + /// + /// # Examples + /// + /// This example uses the [`solana_client`] and [`anyhow`] crates. + /// + /// [`solana_client`]: https://docs.rs/solana-client + /// [`anyhow`]: https://docs.rs/anyhow + /// + /// ``` + /// # use solana_sdk::example_mocks::solana_client; + /// use anyhow::Result; + /// use borsh::{BorshSerialize, BorshDeserialize}; + /// use solana_client::rpc_client::RpcClient; + /// use solana_sdk::{ + /// instruction::Instruction, + /// message::Message, + /// pubkey::Pubkey, + /// signature::{Keypair, Signer}, + /// transaction::Transaction, + /// }; + /// + /// // A custom program instruction. This would typically be defined in + /// // another crate so it can be shared between the on-chain program and + /// // the client. + /// #[derive(BorshSerialize, BorshDeserialize)] + /// enum BankInstruction { + /// Initialize, + /// Deposit { lamports: u64 }, + /// Withdraw { lamports: u64 }, + /// } + /// + /// fn send_initialize_tx( + /// client: &RpcClient, + /// program_id: Pubkey, + /// payer: &Keypair + /// ) -> Result<()> { + /// + /// let bank_instruction = BankInstruction::Initialize; + /// + /// let instruction = Instruction::new_with_borsh( + /// program_id, + /// &bank_instruction, + /// vec![], + /// ); + /// + /// let message = Message::new( + /// &[instruction], + /// Some(&payer.pubkey()), + /// ); + /// + /// let mut tx = Transaction::new_unsigned(message); + /// let blockhash = client.get_latest_blockhash()?; + /// tx.sign(&[payer], blockhash); + /// client.send_and_confirm_transaction(&tx)?; + /// + /// Ok(()) + /// } + /// # + /// # let client = RpcClient::new(String::new()); + /// # let program_id = Pubkey::new_unique(); + /// # let payer = Keypair::new(); + /// # send_initialize_tx(&client, program_id, &payer)?; + /// # + /// # Ok::<(), anyhow::Error>(()) + /// ``` pub fn new_unsigned(message: Message) -> Self { Self { signatures: vec![Signature::default(); message.header.num_required_signatures as usize], @@ -78,31 +275,77 @@ impl Transaction { } } - pub fn new_with_payer(instructions: &[Instruction], payer: Option<&Pubkey>) -> Self { - let message = Message::new(instructions, payer); - Self::new_unsigned(message) - } - - /// Create a signed transaction with the given payer. + /// Create a fully-signed transaction from a [`Message`]. /// /// # Panics /// - /// Panics when signing fails. - pub fn new_signed_with_payer( - instructions: &[Instruction], - payer: Option<&Pubkey>, - signing_keypairs: &T, - recent_blockhash: Hash, - ) -> Self { - let message = Message::new(instructions, payer); - Self::new(signing_keypairs, message, recent_blockhash) - } - - /// Create a signed transaction. + /// Panics when signing fails. See [`Transaction::try_sign`] and + /// [`Transaction::try_partial_sign`] for a full description of failure + /// scenarios. /// - /// # Panics + /// # Examples /// - /// Panics when signing fails. + /// This example uses the [`solana_client`] and [`anyhow`] crates. + /// + /// [`solana_client`]: https://docs.rs/solana-client + /// [`anyhow`]: https://docs.rs/anyhow + /// + /// ``` + /// # use solana_sdk::example_mocks::solana_client; + /// use anyhow::Result; + /// use borsh::{BorshSerialize, BorshDeserialize}; + /// use solana_client::rpc_client::RpcClient; + /// use solana_sdk::{ + /// instruction::Instruction, + /// message::Message, + /// pubkey::Pubkey, + /// signature::{Keypair, Signer}, + /// transaction::Transaction, + /// }; + /// + /// // A custom program instruction. This would typically be defined in + /// // another crate so it can be shared between the on-chain program and + /// // the client. + /// #[derive(BorshSerialize, BorshDeserialize)] + /// enum BankInstruction { + /// Initialize, + /// Deposit { lamports: u64 }, + /// Withdraw { lamports: u64 }, + /// } + /// + /// fn send_initialize_tx( + /// client: &RpcClient, + /// program_id: Pubkey, + /// payer: &Keypair + /// ) -> Result<()> { + /// + /// let bank_instruction = BankInstruction::Initialize; + /// + /// let instruction = Instruction::new_with_borsh( + /// program_id, + /// &bank_instruction, + /// vec![], + /// ); + /// + /// let message = Message::new( + /// &[instruction], + /// Some(&payer.pubkey()), + /// ); + /// + /// let blockhash = client.get_latest_blockhash()?; + /// let mut tx = Transaction::new(&[payer], message, blockhash); + /// client.send_and_confirm_transaction(&tx)?; + /// + /// Ok(()) + /// } + /// # + /// # let client = RpcClient::new(String::new()); + /// # let program_id = Pubkey::new_unique(); + /// # let payer = Keypair::new(); + /// # send_initialize_tx(&client, program_id, &payer)?; + /// # + /// # Ok::<(), anyhow::Error>(()) + /// ``` pub fn new( from_keypairs: &T, message: Message, @@ -113,7 +356,165 @@ impl Transaction { tx } - /// Create a signed transaction + /// Create an unsigned transaction from a list of [`Instruction`]s. + /// + /// `payer` is the account responsible for paying the cost of executing the + /// transaction. It is typically provided, but is optional in some cases. + /// See the [`Transaction`] docs for more. + /// + /// # Examples + /// + /// This example uses the [`solana_client`] and [`anyhow`] crates. + /// + /// [`solana_client`]: https://docs.rs/solana-client + /// [`anyhow`]: https://docs.rs/anyhow + /// + /// ``` + /// # use solana_sdk::example_mocks::solana_client; + /// use anyhow::Result; + /// use borsh::{BorshSerialize, BorshDeserialize}; + /// use solana_client::rpc_client::RpcClient; + /// use solana_sdk::{ + /// instruction::Instruction, + /// message::Message, + /// pubkey::Pubkey, + /// signature::{Keypair, Signer}, + /// transaction::Transaction, + /// }; + /// + /// // A custom program instruction. This would typically be defined in + /// // another crate so it can be shared between the on-chain program and + /// // the client. + /// #[derive(BorshSerialize, BorshDeserialize)] + /// enum BankInstruction { + /// Initialize, + /// Deposit { lamports: u64 }, + /// Withdraw { lamports: u64 }, + /// } + /// + /// fn send_initialize_tx( + /// client: &RpcClient, + /// program_id: Pubkey, + /// payer: &Keypair + /// ) -> Result<()> { + /// + /// let bank_instruction = BankInstruction::Initialize; + /// + /// let instruction = Instruction::new_with_borsh( + /// program_id, + /// &bank_instruction, + /// vec![], + /// ); + /// + /// let mut tx = Transaction::new_with_payer(&[instruction], Some(&payer.pubkey())); + /// let blockhash = client.get_latest_blockhash()?; + /// tx.sign(&[payer], blockhash); + /// client.send_and_confirm_transaction(&tx)?; + /// + /// Ok(()) + /// } + /// # + /// # let client = RpcClient::new(String::new()); + /// # let program_id = Pubkey::new_unique(); + /// # let payer = Keypair::new(); + /// # send_initialize_tx(&client, program_id, &payer)?; + /// # + /// # Ok::<(), anyhow::Error>(()) + /// ``` + pub fn new_with_payer(instructions: &[Instruction], payer: Option<&Pubkey>) -> Self { + let message = Message::new(instructions, payer); + Self::new_unsigned(message) + } + + /// Create a fully-signed transaction from a list of [`Instruction`]s. + /// + /// `payer` is the account responsible for paying the cost of executing the + /// transaction. It is typically provided, but is optional in some cases. + /// See the [`Transaction`] docs for more. + /// + /// # Panics + /// + /// Panics when signing fails. See [`Transaction::try_sign`] and + /// [`Transaction::try_partial_sign`] for a full description of failure + /// scenarios. + /// + /// # Examples + /// + /// This example uses the [`solana_client`] and [`anyhow`] crates. + /// + /// [`solana_client`]: https://docs.rs/solana-client + /// [`anyhow`]: https://docs.rs/anyhow + /// + /// ``` + /// # use solana_sdk::example_mocks::solana_client; + /// use anyhow::Result; + /// use borsh::{BorshSerialize, BorshDeserialize}; + /// use solana_client::rpc_client::RpcClient; + /// use solana_sdk::{ + /// instruction::Instruction, + /// message::Message, + /// pubkey::Pubkey, + /// signature::{Keypair, Signer}, + /// transaction::Transaction, + /// }; + /// + /// // A custom program instruction. This would typically be defined in + /// // another crate so it can be shared between the on-chain program and + /// // the client. + /// #[derive(BorshSerialize, BorshDeserialize)] + /// enum BankInstruction { + /// Initialize, + /// Deposit { lamports: u64 }, + /// Withdraw { lamports: u64 }, + /// } + /// + /// fn send_initialize_tx( + /// client: &RpcClient, + /// program_id: Pubkey, + /// payer: &Keypair + /// ) -> Result<()> { + /// + /// let bank_instruction = BankInstruction::Initialize; + /// + /// let instruction = Instruction::new_with_borsh( + /// program_id, + /// &bank_instruction, + /// vec![], + /// ); + /// + /// let blockhash = client.get_latest_blockhash()?; + /// let mut tx = Transaction::new_signed_with_payer( + /// &[instruction], + /// Some(&payer.pubkey()), + /// &[payer], + /// blockhash, + /// ); + /// client.send_and_confirm_transaction(&tx)?; + /// + /// Ok(()) + /// } + /// # + /// # let client = RpcClient::new(String::new()); + /// # let program_id = Pubkey::new_unique(); + /// # let payer = Keypair::new(); + /// # send_initialize_tx(&client, program_id, &payer)?; + /// # + /// # Ok::<(), anyhow::Error>(()) + /// ``` + pub fn new_signed_with_payer( + instructions: &[Instruction], + payer: Option<&Pubkey>, + signing_keypairs: &T, + recent_blockhash: Hash, + ) -> Self { + let message = Message::new(instructions, payer); + Self::new(signing_keypairs, message, recent_blockhash) + } + + /// Create a fully-signed transaction from pre-compiled instructions. + /// + /// # Arguments + /// /// * `from_keypairs` - The keys used to sign the transaction. /// * `keys` - The keys for the transaction. These are the program state /// instances or lamport recipient keys. @@ -123,7 +524,8 @@ impl Transaction { /// /// # Panics /// - /// Panics when signing fails. + /// Panics when signing fails. See [`Transaction::try_sign`] and for a full + /// description of failure conditions. pub fn new_with_compiled_instructions( from_keypairs: &T, keys: &[Pubkey], @@ -146,6 +548,17 @@ impl Transaction { Transaction::new(from_keypairs, message, recent_blockhash) } + /// Get the data for an instruction at the given index. + /// + /// The `instruction_index` corresponds to the [`instructions`] vector of + /// the `Transaction`'s [`Message`] value. + /// + /// [`instructions`]: Message::instructions + /// + /// # Panics + /// + /// Panics if `instruction_index` is greater than or equal to the number of + /// instructions in the transaction. pub fn data(&self, instruction_index: usize) -> &[u8] { &self.message.instructions[instruction_index].data } @@ -158,11 +571,41 @@ impl Transaction { .map(|&account_keys_index| account_keys_index as usize) } + /// Get the `Pubkey` of an account required by one of the instructions in + /// the transaction. + /// + /// The `instruction_index` corresponds to the [`instructions`] vector of + /// the `Transaction`'s [`Message`] value; and the `account_index` to the + /// [`accounts`] vector of the message's [`CompiledInstruction`]s. + /// + /// [`instructions`]: Message::instructions + /// [`accounts`]: CompiledInstruction::accounts + /// [`CompiledInstruction`]: CompiledInstruction + /// + /// Returns `None` if `instruction_index` is greater than or equal to the + /// number of instructions in the transaction; or if `accounts_index` is + /// greater than or equal to the number of accounts in the instruction. pub fn key(&self, instruction_index: usize, accounts_index: usize) -> Option<&Pubkey> { self.key_index(instruction_index, accounts_index) .and_then(|account_keys_index| self.message.account_keys.get(account_keys_index)) } + /// Get the `Pubkey` of a signing account required by one of the + /// instructions in the transaction. + /// + /// The transaction does not need to be signed for this function to return a + /// signing account's pubkey. + /// + /// Returns `None` if the indexed account is not required to sign the + /// transaction. Returns `None` if the [`signatures`] field does not contain + /// enough elements to hold a signature for the indexed account (this should + /// only be possible if `Transaction` has been manually constructed). + /// + /// [`signatures`]: Transaction::signatures + /// + /// Returns `None` if `instruction_index` is greater than or equal to the + /// number of instructions in the transaction; or if `accounts_index` is + /// greater than or equal to the number of accounts in the instruction. pub fn signer_key(&self, instruction_index: usize, accounts_index: usize) -> Option<&Pubkey> { match self.key_index(instruction_index, accounts_index) { None => None, @@ -175,7 +618,7 @@ impl Transaction { } } - /// Return a message containing all data that should be signed. + /// Return the message containing all data that should be signed. pub fn message(&self) -> &Message { &self.message } @@ -185,36 +628,128 @@ impl Transaction { self.message().serialize() } - /// Check keys and keypair lengths, then sign this transaction. + /// Sign the transaction. + /// + /// This method fully signs a transaction with all required signers, which + /// must be present in the `keypairs` slice. To sign with only some of the + /// required signers, use [`Transaction::partial_sign`]. + /// + /// If `recent_blockhash` is different than recorded in the transaction message's + /// [`recent_blockhash`] field, then the message's `recent_blockhash` will be updated + /// to the provided `recent_blockhash`, and any prior signatures will be cleared. + /// + /// [`recent_blockhash`]: Message::recent_blockhash /// /// # Panics /// - /// Panics when signing fails, use [`Transaction::try_sign`] to handle the error. + /// Panics when signing fails. Use [`Transaction::try_sign`] to handle the + /// error. See the documentation for [`Transaction::try_sign`] for a full description of + /// failure conditions. + /// + /// # Examples + /// + /// This example uses the [`solana_client`] and [`anyhow`] crates. + /// + /// [`solana_client`]: https://docs.rs/solana-client + /// [`anyhow`]: https://docs.rs/anyhow + /// + /// ``` + /// # use solana_sdk::example_mocks::solana_client; + /// use anyhow::Result; + /// use borsh::{BorshSerialize, BorshDeserialize}; + /// use solana_client::rpc_client::RpcClient; + /// use solana_sdk::{ + /// instruction::Instruction, + /// message::Message, + /// pubkey::Pubkey, + /// signature::{Keypair, Signer}, + /// transaction::Transaction, + /// }; + /// + /// // A custom program instruction. This would typically be defined in + /// // another crate so it can be shared between the on-chain program and + /// // the client. + /// #[derive(BorshSerialize, BorshDeserialize)] + /// enum BankInstruction { + /// Initialize, + /// Deposit { lamports: u64 }, + /// Withdraw { lamports: u64 }, + /// } + /// + /// fn send_initialize_tx( + /// client: &RpcClient, + /// program_id: Pubkey, + /// payer: &Keypair + /// ) -> Result<()> { + /// + /// let bank_instruction = BankInstruction::Initialize; + /// + /// let instruction = Instruction::new_with_borsh( + /// program_id, + /// &bank_instruction, + /// vec![], + /// ); + /// + /// let mut tx = Transaction::new_with_payer(&[instruction], Some(&payer.pubkey())); + /// let blockhash = client.get_latest_blockhash()?; + /// tx.sign(&[payer], blockhash); + /// client.send_and_confirm_transaction(&tx)?; + /// + /// Ok(()) + /// } + /// # + /// # let client = RpcClient::new(String::new()); + /// # let program_id = Pubkey::new_unique(); + /// # let payer = Keypair::new(); + /// # send_initialize_tx(&client, program_id, &payer)?; + /// # + /// # Ok::<(), anyhow::Error>(()) + /// ``` pub fn sign(&mut self, keypairs: &T, recent_blockhash: Hash) { if let Err(e) = self.try_sign(keypairs, recent_blockhash) { panic!("Transaction::sign failed with error {:?}", e); } } - /// Sign using some subset of required keys - /// if recent_blockhash is not the same as currently in the transaction, - /// clear any prior signatures and update recent_blockhash + /// Sign the transaction with a subset of required keys. + /// + /// Unlike [`Transaction::sign`], this method does not require all keypairs + /// to be provided, allowing a transaction to be signed in multiple steps. + /// + /// It is permitted to sign a transaction with the same keypair multiple + /// times. + /// + /// If `recent_blockhash` is different than recorded in the transaction message's + /// [`recent_blockhash`] field, then the message's `recent_blockhash` will be updated + /// to the provided `recent_blockhash`, and any prior signatures will be cleared. + /// + /// [`recent_blockhash`]: Message::recent_blockhash /// /// # Panics /// - /// Panics when signing fails, use [`Transaction::try_partial_sign`] to handle the error. + /// Panics when signing fails. Use [`Transaction::try_partial_sign`] to + /// handle the error. See the documentation for + /// [`Transaction::try_partial_sign`] for a full description of failure + /// conditions. pub fn partial_sign(&mut self, keypairs: &T, recent_blockhash: Hash) { if let Err(e) = self.try_partial_sign(keypairs, recent_blockhash) { panic!("Transaction::partial_sign failed with error {:?}", e); } } - /// Sign the transaction and place the signatures in their associated positions in `signatures` - /// without checking that the positions are correct. + /// Sign the transaction with a subset of required keys. + /// + /// This places each of the signatures created from `keypairs` in the + /// corresponding position, as specified in the `positions` vector, in the + /// transactions [`signatures`] field. It does not verify that the signature + /// positions are correct. + /// + /// [`signatures`]: Transaction::signatures /// /// # Panics /// - /// Panics when signing fails, use [`Transaction::try_partial_sign_unchecked`] to handle the error. + /// Panics if signing fails. Use [`Transaction::try_partial_sign_unchecked`] + /// to handle the error. pub fn partial_sign_unchecked( &mut self, keypairs: &T, @@ -229,8 +764,88 @@ impl Transaction { } } - /// Check keys and keypair lengths, then sign this transaction, returning any signing errors - /// encountered + /// Sign the transaction, returning any errors. + /// + /// This method fully signs a transaction with all required signers, which + /// must be present in the `keypairs` slice. To sign with only some of the + /// required signers, use [`Transaction::try_partial_sign`]. + /// + /// If `recent_blockhash` is different than recorded in the transaction message's + /// [`recent_blockhash`] field, then the message's `recent_blockhash` will be updated + /// to the provided `recent_blockhash`, and any prior signatures will be cleared. + /// + /// [`recent_blockhash`]: Message::recent_blockhash + /// + /// # Errors + /// + /// Signing will fail if some required signers are not provided in + /// `keypairs`; or, if the transaction has previously been partially signed, + /// some of the remaining required signers are not provided in `keypairs`. + /// In other words, the transaction must be fully signed as a result of + /// calling this function. The error is [`SignerError::NotEnoughSigners`]. + /// + /// Signing will fail for any of the reasons described in the documentation + /// for [`Transaction::try_partial_sign`]. + /// + /// # Examples + /// + /// This example uses the [`solana_client`] and [`anyhow`] crates. + /// + /// [`solana_client`]: https://docs.rs/solana-client + /// [`anyhow`]: https://docs.rs/anyhow + /// + /// ``` + /// # use solana_sdk::example_mocks::solana_client; + /// use anyhow::Result; + /// use borsh::{BorshSerialize, BorshDeserialize}; + /// use solana_client::rpc_client::RpcClient; + /// use solana_sdk::{ + /// instruction::Instruction, + /// message::Message, + /// pubkey::Pubkey, + /// signature::{Keypair, Signer}, + /// transaction::Transaction, + /// }; + /// + /// // A custom program instruction. This would typically be defined in + /// // another crate so it can be shared between the on-chain program and + /// // the client. + /// #[derive(BorshSerialize, BorshDeserialize)] + /// enum BankInstruction { + /// Initialize, + /// Deposit { lamports: u64 }, + /// Withdraw { lamports: u64 }, + /// } + /// + /// fn send_initialize_tx( + /// client: &RpcClient, + /// program_id: Pubkey, + /// payer: &Keypair + /// ) -> Result<()> { + /// + /// let bank_instruction = BankInstruction::Initialize; + /// + /// let instruction = Instruction::new_with_borsh( + /// program_id, + /// &bank_instruction, + /// vec![], + /// ); + /// + /// let mut tx = Transaction::new_with_payer(&[instruction], Some(&payer.pubkey())); + /// let blockhash = client.get_latest_blockhash()?; + /// tx.try_sign(&[payer], blockhash)?; + /// client.send_and_confirm_transaction(&tx)?; + /// + /// Ok(()) + /// } + /// # + /// # let client = RpcClient::new(String::new()); + /// # let program_id = Pubkey::new_unique(); + /// # let payer = Keypair::new(); + /// # send_initialize_tx(&client, program_id, &payer)?; + /// # + /// # Ok::<(), anyhow::Error>(()) + /// ``` pub fn try_sign( &mut self, keypairs: &T, @@ -245,9 +860,55 @@ impl Transaction { } } - /// Sign using some subset of required keys, returning any signing errors encountered. If - /// recent_blockhash is not the same as currently in the transaction, clear any prior - /// signatures and update recent_blockhash + /// Sign the transaction with a subset of required keys, returning any errors. + /// + /// Unlike [`Transaction::try_sign`], this method does not require all + /// keypairs to be provided, allowing a transaction to be signed in multiple + /// steps. + /// + /// It is permitted to sign a transaction with the same keypair multiple + /// times. + /// + /// If `recent_blockhash` is different than recorded in the transaction message's + /// [`recent_blockhash`] field, then the message's `recent_blockhash` will be updated + /// to the provided `recent_blockhash`, and any prior signatures will be cleared. + /// + /// [`recent_blockhash`]: Message::recent_blockhash + /// + /// # Errors + /// + /// Signing will fail if + /// + /// - The transaction's [`Message`] is malformed such that the number of + /// required signatures recorded in its header + /// ([`num_required_signatures`]) is greater than the length of its + /// account keys ([`account_keys`]). The error is + /// [`SignerError::TransactionError`] where the interior + /// [`TransactionError`] is [`TransactionError::InvalidAccountIndex`]. + /// - Any of the provided signers in `keypairs` is not a required signer of + /// the message. The error is [`SignerError::KeypairPubkeyMismatch`]. + /// - Any of the signers is a [`Presigner`], and its provided signature is + /// incorrect. The error is [`SignerError::PresignerError`] where the + /// interior [`PresignerError`] is + /// [`PresignerError::VerificationFailure`]. + /// - The signer is a [`RemoteKeypair`] and + /// - It does not understand the input provided ([`SignerError::InvalidInput`]). + /// - The device cannot be found ([`SignerError::NoDeviceFound`]). + /// - The user cancels the signing ([`SignerError::UserCancel`]). + /// - An error was encountered connecting ([`SignerError::Connection`]). + /// - Some device-specific protocol error occurs ([`SignerError::Protocol`]). + /// - Some other error occurs ([`SignerError::Custom`]). + /// + /// See the documentation for the [`solana-remote-wallet`] crate for details + /// on the operation of [`RemoteKeypair`] signers. + /// + /// [`num_required_signatures`]: crate::message::MessageHeader::num_required_signatures + /// [`account_keys`]: Message::account_keys + /// [`Presigner`]: crate::signer::presigner::Presigner + /// [`PresignerError`]: crate::signer::presigner::PresignerError + /// [`PresignerError::VerificationFailure`]: crate::signer::presigner::PresignerError::VerificationFailure + /// [`solana-remote-wallet`]: https://docs.rs/solana-remote-wallet/latest/ + /// [`RemoteKeypair`]: https://docs.rs/solana-remote-wallet/latest/solana_remote_wallet/remote_keypair/struct.RemoteKeypair.html pub fn try_partial_sign( &mut self, keypairs: &T, @@ -261,9 +922,19 @@ impl Transaction { self.try_partial_sign_unchecked(keypairs, positions, recent_blockhash) } - /// Sign the transaction, returning any signing errors encountered, and place the - /// signatures in their associated positions in `signatures` without checking that the + /// Sign the transaction with a subset of required keys, returning any + /// errors. + /// + /// This places each of the signatures created from `keypairs` in the + /// corresponding position, as specified in the `positions` vector, in the + /// transactions [`signatures`] field. It does not verify that the signature /// positions are correct. + /// + /// [`signatures`]: Transaction::signatures + /// + /// # Errors + /// + /// Returns an error if signing fails. pub fn try_partial_sign_unchecked( &mut self, keypairs: &T, @@ -285,7 +956,16 @@ impl Transaction { Ok(()) } - /// Verify the transaction + /// Returns a signature that is not valid for signing this transaction. + pub fn get_invalid_signature() -> Signature { + Signature::default() + } + + /// Verifies that all signers have signed the message. + /// + /// # Errors + /// + /// Returns [`TransactionError::SignatureFailure`] on error. pub fn verify(&self) -> Result<()> { let message_bytes = self.message_data(); if !self @@ -299,11 +979,11 @@ impl Transaction { } } - pub fn get_invalid_signature() -> Signature { - Signature::default() - } - - /// Verify the transaction and hash its message + /// Verify the transaction and hash its message. + /// + /// # Errors + /// + /// Returns [`TransactionError::SignatureFailure`] on error. pub fn verify_and_hash_message(&self) -> Result { let message_bytes = self.message_data(); if !self @@ -317,6 +997,10 @@ impl Transaction { } } + /// Verifies that all signers have signed the message. + /// + /// Returns a vector with the length of required signatures, where each + /// element is either `true` if that signer has signed, or `false` if not. pub fn verify_with_results(&self) -> Vec { self._verify_with_results(&self.message_data()) } @@ -329,7 +1013,7 @@ impl Transaction { .collect() } - /// Verify the precompiled programs in this transaction + /// Verify the precompiled programs in this transaction. pub fn verify_precompiles(&self, feature_set: &Arc) -> Result<()> { for instruction in &self.message().instructions { // The Transaction may not be sanitized at this point @@ -349,7 +1033,9 @@ impl Transaction { Ok(()) } - /// Get the positions of the pubkeys in `account_keys` associated with signing keypairs + /// Get the positions of the pubkeys in `account_keys` associated with signing keypairs. + /// + /// [`account_keys`]: Message::account_keys pub fn get_signing_keypair_positions(&self, pubkeys: &[Pubkey]) -> Result>> { if self.message.account_keys.len() < self.message.header.num_required_signatures as usize { return Err(TransactionError::InvalidAccountIndex); @@ -363,7 +1049,7 @@ impl Transaction { .collect()) } - /// Replace all the signatures and pubkeys + /// Replace all the signatures and pubkeys. pub fn replace_signatures(&mut self, signers: &[(Pubkey, Signature)]) -> Result<()> { let num_required_signatures = self.message.header.num_required_signatures as usize; if signers.len() != num_required_signatures