use crate::{ decode_error::DecodeError, instruction::{AccountMeta, Instruction}, nonce, pubkey::Pubkey, system_program, sysvar::{recent_blockhashes, rent}, }; use num_derive::{FromPrimitive, ToPrimitive}; use thiserror::Error; #[derive(Error, Debug, Serialize, Clone, PartialEq, FromPrimitive, ToPrimitive)] pub enum SystemError { #[error("an account with the same address already exists")] AccountAlreadyInUse, #[error("account does not have enough SOL to perform the operation")] ResultWithNegativeLamports, #[error("cannot assign account to this program id")] InvalidProgramId, #[error("cannot allocate account data of this length")] InvalidAccountDataLength, #[error("length of requested seed is too long")] MaxSeedLengthExceeded, #[error("provided address does not match addressed derived from seed")] AddressWithSeedMismatch, } impl DecodeError for SystemError { fn type_of() -> &'static str { "SystemError" } } #[derive(Error, Debug, Clone, PartialEq, FromPrimitive, ToPrimitive)] pub enum NonceError { #[error("recent blockhash list is empty")] NoRecentBlockhashes, #[error("stored nonce is still in recent_blockhashes")] NotExpired, #[error("specified nonce does not match stored nonce")] UnexpectedValue, #[error("cannot handle request in current account state")] BadAccountState, } impl DecodeError for NonceError { fn type_of() -> &'static str { "NonceError" } } /// maximum permitted size of data: 10 MB pub const MAX_PERMITTED_DATA_LENGTH: u64 = 10 * 1024 * 1024; #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub enum SystemInstruction { /// Create a new account /// /// # Account references /// 0. [WRITE, SIGNER] Funding account /// 1. [WRITE, SIGNER] New account CreateAccount { /// Number of lamports to transfer to the new account lamports: u64, /// Number of bytes of memory to allocate space: u64, /// Address of program that will own the new account owner: Pubkey, }, /// Assign account to a program /// /// # Account references /// 0. [WRITE, SIGNER] Assigned account public key Assign { /// Owner program account owner: Pubkey, }, /// Transfer lamports /// /// # Account references /// 0. [WRITE, SIGNER] Funding account /// 1. [WRITE] Recipient account Transfer { lamports: u64 }, /// Create a new account at an address derived from a base pubkey and a seed /// /// # Account references /// 0. [WRITE, SIGNER] Funding account /// 1. [WRITE] Created account /// 2. [SIGNER] Base account CreateAccountWithSeed { /// Base public key base: Pubkey, /// String of ASCII chars, no longer than `Pubkey::MAX_SEED_LEN` seed: String, /// Number of lamports to transfer to the new account lamports: u64, /// Number of bytes of memory to allocate space: u64, /// Owner program account address owner: Pubkey, }, /// Consumes a stored nonce, replacing it with a successor /// /// # Account references /// 0. [WRITE, SIGNER] Nonce account /// 1. [] RecentBlockhashes sysvar /// 2. [SIGNER] Nonce authority AdvanceNonceAccount, /// Withdraw funds from a nonce account /// /// # Account references /// 0. [WRITE] Nonce account /// 1. [WRITE] Recipient account /// 2. [] RecentBlockhashes sysvar /// 3. [] Rent sysvar /// 4. [SIGNER] Nonce authority /// /// The `u64` parameter is the lamports to withdraw, which must leave the /// account balance above the rent exempt reserve or at zero. WithdrawNonceAccount(u64), /// Drive state of Uninitalized nonce account to Initialized, setting the nonce value /// /// # Account references /// 0. [WRITE] Nonce account /// 1. [] RecentBlockhashes sysvar /// 2. [] Rent sysvar /// /// The `Pubkey` parameter specifies the entity authorized to execute nonce /// instruction on the account /// /// No signatures are required to execute this instruction, enabling derived /// nonce account addresses InitializeNonceAccount(Pubkey), /// Change the entity authorized to execute nonce instructions on the account /// /// # Account references /// 0. [WRITE, SIGNER] Nonce account /// /// The `Pubkey` parameter identifies the entity to authorize AuthorizeNonceAccount(Pubkey), /// Allocate space in a (possibly new) account without funding /// /// # Account references /// 0. [WRITE, SIGNER] New account Allocate { /// Number of bytes of memory to allocate space: u64, }, /// Allocate space for and assign an account at an address /// derived from a base public key and a seed /// /// # Account references /// 0. [WRITE] Allocated account /// 1. [SIGNER] Base account AllocateWithSeed { /// Base public key base: Pubkey, /// String of ASCII chars, no longer than `pubkey::MAX_SEED_LEN` seed: String, /// Number of bytes of memory to allocate space: u64, /// Owner program account owner: Pubkey, }, /// Assign account to a program based on a seed /// /// # Account references /// 0. [WRITE] Assigned account /// 1. [SIGNER] Base account AssignWithSeed { /// Base public key base: Pubkey, /// String of ASCII chars, no longer than `pubkey::MAX_SEED_LEN` seed: String, /// Owner program account owner: Pubkey, }, } pub fn create_account( from_pubkey: &Pubkey, to_pubkey: &Pubkey, lamports: u64, space: u64, owner: &Pubkey, ) -> Instruction { let account_metas = vec![ AccountMeta::new(*from_pubkey, true), AccountMeta::new(*to_pubkey, true), ]; Instruction::new( system_program::id(), &SystemInstruction::CreateAccount { lamports, space, owner: *owner, }, account_metas, ) } // we accept `to` as a parameter so that callers do their own error handling when // calling create_address_with_seed() pub fn create_account_with_seed( from_pubkey: &Pubkey, to_pubkey: &Pubkey, // must match create_address_with_seed(base, seed, owner) base: &Pubkey, seed: &str, lamports: u64, space: u64, owner: &Pubkey, ) -> Instruction { let account_metas = vec![ AccountMeta::new(*from_pubkey, true), AccountMeta::new(*to_pubkey, false), AccountMeta::new_readonly(*base, true), ]; Instruction::new( system_program::id(), &SystemInstruction::CreateAccountWithSeed { base: *base, seed: seed.to_string(), lamports, space, owner: *owner, }, account_metas, ) } pub fn assign(pubkey: &Pubkey, owner: &Pubkey) -> Instruction { let account_metas = vec![AccountMeta::new(*pubkey, true)]; Instruction::new( system_program::id(), &SystemInstruction::Assign { owner: *owner }, account_metas, ) } pub fn assign_with_seed( address: &Pubkey, // must match create_address_with_seed(base, seed, owner) base: &Pubkey, seed: &str, owner: &Pubkey, ) -> Instruction { let account_metas = vec![ AccountMeta::new(*address, false), AccountMeta::new_readonly(*base, true), ]; Instruction::new( system_program::id(), &SystemInstruction::AssignWithSeed { base: *base, seed: seed.to_string(), owner: *owner, }, account_metas, ) } pub fn transfer(from_pubkey: &Pubkey, to_pubkey: &Pubkey, lamports: u64) -> Instruction { let account_metas = vec![ AccountMeta::new(*from_pubkey, true), AccountMeta::new(*to_pubkey, false), ]; Instruction::new( system_program::id(), &SystemInstruction::Transfer { lamports }, account_metas, ) } pub fn allocate(pubkey: &Pubkey, space: u64) -> Instruction { let account_metas = vec![AccountMeta::new(*pubkey, true)]; Instruction::new( system_program::id(), &SystemInstruction::Allocate { space }, account_metas, ) } pub fn allocate_with_seed( address: &Pubkey, // must match create_address_with_seed(base, seed, owner) base: &Pubkey, seed: &str, space: u64, owner: &Pubkey, ) -> Instruction { let account_metas = vec![ AccountMeta::new(*address, false), AccountMeta::new_readonly(*base, true), ]; Instruction::new( system_program::id(), &SystemInstruction::AllocateWithSeed { base: *base, seed: seed.to_string(), space, owner: *owner, }, account_metas, ) } /// Create and sign new SystemInstruction::Transfer transaction to many destinations pub fn transfer_many(from_pubkey: &Pubkey, to_lamports: &[(Pubkey, u64)]) -> Vec { to_lamports .iter() .map(|(to_pubkey, lamports)| transfer(from_pubkey, to_pubkey, *lamports)) .collect() } pub fn create_nonce_account_with_seed( from_pubkey: &Pubkey, nonce_pubkey: &Pubkey, base: &Pubkey, seed: &str, authority: &Pubkey, lamports: u64, ) -> Vec { vec![ create_account_with_seed( from_pubkey, nonce_pubkey, base, seed, lamports, nonce::State::size() as u64, &system_program::id(), ), Instruction::new( system_program::id(), &SystemInstruction::InitializeNonceAccount(*authority), vec![ AccountMeta::new(*nonce_pubkey, false), AccountMeta::new_readonly(recent_blockhashes::id(), false), AccountMeta::new_readonly(rent::id(), false), ], ), ] } pub fn create_nonce_account( from_pubkey: &Pubkey, nonce_pubkey: &Pubkey, authority: &Pubkey, lamports: u64, ) -> Vec { vec![ create_account( from_pubkey, nonce_pubkey, lamports, nonce::State::size() as u64, &system_program::id(), ), Instruction::new( system_program::id(), &SystemInstruction::InitializeNonceAccount(*authority), vec![ AccountMeta::new(*nonce_pubkey, false), AccountMeta::new_readonly(recent_blockhashes::id(), false), AccountMeta::new_readonly(rent::id(), false), ], ), ] } pub fn advance_nonce_account(nonce_pubkey: &Pubkey, authorized_pubkey: &Pubkey) -> Instruction { let account_metas = vec![ AccountMeta::new(*nonce_pubkey, false), AccountMeta::new_readonly(recent_blockhashes::id(), false), AccountMeta::new_readonly(*authorized_pubkey, true), ]; Instruction::new( system_program::id(), &SystemInstruction::AdvanceNonceAccount, account_metas, ) } pub fn withdraw_nonce_account( nonce_pubkey: &Pubkey, authorized_pubkey: &Pubkey, to_pubkey: &Pubkey, lamports: u64, ) -> Instruction { let account_metas = vec![ AccountMeta::new(*nonce_pubkey, false), AccountMeta::new(*to_pubkey, false), AccountMeta::new_readonly(recent_blockhashes::id(), false), AccountMeta::new_readonly(rent::id(), false), AccountMeta::new_readonly(*authorized_pubkey, true), ]; Instruction::new( system_program::id(), &SystemInstruction::WithdrawNonceAccount(lamports), account_metas, ) } pub fn authorize_nonce_account( nonce_pubkey: &Pubkey, authorized_pubkey: &Pubkey, new_authority: &Pubkey, ) -> Instruction { let account_metas = vec![ AccountMeta::new(*nonce_pubkey, false), AccountMeta::new_readonly(*authorized_pubkey, true), ]; Instruction::new( system_program::id(), &SystemInstruction::AuthorizeNonceAccount(*new_authority), account_metas, ) } #[cfg(test)] mod tests { use super::*; use crate::instruction::{Instruction, InstructionError}; fn get_keys(instruction: &Instruction) -> Vec { instruction.accounts.iter().map(|x| x.pubkey).collect() } #[test] fn test_move_many() { let alice_pubkey = Pubkey::new_rand(); let bob_pubkey = Pubkey::new_rand(); let carol_pubkey = Pubkey::new_rand(); let to_lamports = vec![(bob_pubkey, 1), (carol_pubkey, 2)]; let instructions = transfer_many(&alice_pubkey, &to_lamports); assert_eq!(instructions.len(), 2); assert_eq!(get_keys(&instructions[0]), vec![alice_pubkey, bob_pubkey]); assert_eq!(get_keys(&instructions[1]), vec![alice_pubkey, carol_pubkey]); } #[test] fn test_create_nonce_account() { let from_pubkey = Pubkey::new_rand(); let nonce_pubkey = Pubkey::new_rand(); let authorized = nonce_pubkey; let ixs = create_nonce_account(&from_pubkey, &nonce_pubkey, &authorized, 42); assert_eq!(ixs.len(), 2); let ix = &ixs[0]; assert_eq!(ix.program_id, system_program::id()); let pubkeys: Vec<_> = ix.accounts.iter().map(|am| am.pubkey).collect(); assert!(pubkeys.contains(&from_pubkey)); assert!(pubkeys.contains(&nonce_pubkey)); } #[test] fn test_nonce_error_decode() { use num_traits::FromPrimitive; fn pretty_err(err: InstructionError) -> String where T: 'static + std::error::Error + DecodeError + FromPrimitive, { if let InstructionError::Custom(code) = err { let specific_error: T = T::decode_custom_error_to_enum(code).unwrap(); format!( "{:?}: {}::{:?} - {}", err, T::type_of(), specific_error, specific_error, ) } else { "".to_string() } } assert_eq!( "Custom(0): NonceError::NoRecentBlockhashes - recent blockhash list is empty", pretty_err::(NonceError::NoRecentBlockhashes.into()) ); assert_eq!( "Custom(1): NonceError::NotExpired - stored nonce is still in recent_blockhashes", pretty_err::(NonceError::NotExpired.into()) ); assert_eq!( "Custom(2): NonceError::UnexpectedValue - specified nonce does not match stored nonce", pretty_err::(NonceError::UnexpectedValue.into()) ); assert_eq!( "Custom(3): NonceError::BadAccountState - cannot handle request in current account state", pretty_err::(NonceError::BadAccountState.into()) ); } }