diff --git a/docs/src/apps/jsonrpc-api.md b/docs/src/apps/jsonrpc-api.md index 2c450eee4..a9792efab 100644 --- a/docs/src/apps/jsonrpc-api.md +++ b/docs/src/apps/jsonrpc-api.md @@ -190,7 +190,7 @@ Returns all information associated with the account of provided Pubkey - `` - (optional) Configuration object containing the following optional fields: - (optional) [Commitment](jsonrpc-api.md#configuring-state-commitment) - `encoding: ` - encoding for Account data, either "base58" (*slow*), "base64", or jsonParsed". "base58" is limited to Account data of less than 128 bytes. "base64" will return base64 encoded data for Account data of any size. - Parsed-JSON encoding attempts to use program-specific state parsers to return more human-readable and explicit account state data. If parsed-JSON is requested but a parser cannot be found, the field falls back to base64 encoding, detectable when the `data` field is type ``. **jsonParsed encoding is UNSTABLE** + Parsed-JSON encoding attempts to use program-specific state parsers to return more human-readable and explicit account state data. If parsed-JSON is requested but a parser cannot be found, the field falls back to base64 encoding, detectable when the `data` field is type ``. - (optional) `dataSlice: ` - limit the returned account data using the provided `offset: ` and `length: ` fields; only available for "base58" or "base64" encoding. #### Results: @@ -446,7 +446,7 @@ Returns identity and transaction information about a confirmed block in the ledg #### Parameters: - `` - slot, as u64 integer -- `` - encoding for each returned Transaction, either "json", "jsonParsed", "base58" (*slow*), or "base64". If parameter not provided, the default encoding is JSON. **jsonParsed encoding is UNSTABLE** +- `` - encoding for each returned Transaction, either "json", "jsonParsed", "base58" (*slow*), or "base64". If parameter not provided, the default encoding is JSON. Parsed-JSON encoding attempts to use program-specific instruction parsers to return more human-readable and explicit data in the `transaction.message.instructions` list. If parsed-JSON is requested but a parser cannot be found, the instruction falls back to regular JSON encoding (`accounts`, `data`, and `programIdIndex` fields). #### Results: @@ -816,7 +816,7 @@ Returns transaction details for a confirmed transaction - `` - transaction signature as base-58 encoded string N encoding attempts to use program-specific instruction parsers to return more human-readable and explicit data in the `transaction.message.instructions` list. If parsed-JSON is requested but a parser cannot be found, the instruction falls back to regular JSON encoding (`accounts`, `data`, and `programIdIndex` fields). -- `` - (optional) encoding for the returned Transaction, either "json", "jsonParsed", "base58" (*slow*), or "base64". If parameter not provided, the default encoding is JSON. **jsonParsed encoding is UNSTABLE** +- `` - (optional) encoding for the returned Transaction, either "json", "jsonParsed", "base58" (*slow*), or "base64". If parameter not provided, the default encoding is JSON. #### Results: @@ -1539,7 +1539,7 @@ Returns the account information for a list of Pubkeys - `` - (optional) Configuration object containing the following optional fields: - (optional) [Commitment](jsonrpc-api.md#configuring-state-commitment) - `encoding: ` - encoding for Account data, either "base58" (*slow*), "base64", or jsonParsed". "base58" is limited to Account data of less than 128 bytes. "base64" will return base64 encoded data for Account data of any size. - Parsed-JSON encoding attempts to use program-specific state parsers to return more human-readable and explicit account state data. If parsed-JSON is requested but a parser cannot be found, the field falls back to base64 encoding, detectable when the `data` field is type ``. **jsonParsed encoding is UNSTABLE** + Parsed-JSON encoding attempts to use program-specific state parsers to return more human-readable and explicit account state data. If parsed-JSON is requested but a parser cannot be found, the field falls back to base64 encoding, detectable when the `data` field is type ``. - (optional) `dataSlice: ` - limit the returned account data using the provided `offset: ` and `length: ` fields; only available for "base58" or "base64" encoding. #### Results: @@ -1682,7 +1682,7 @@ Returns all accounts owned by the provided program Pubkey - `` - (optional) Configuration object containing the following optional fields: - (optional) [Commitment](jsonrpc-api.md#configuring-state-commitment) - `encoding: ` - encoding for Account data, either "base58" (*slow*), "base64" or jsonParsed". - Parsed-JSON encoding attempts to use program-specific state parsers to return more human-readable and explicit account state data. If parsed-JSON is requested but a parser cannot be found, the field falls back to base64 encoding, detectable when the `data` field is type ``. If parsed-JSON is requested for the SPL Token program, when a valid mint cannot be found for a particular account, that account will be filtered out from results. **jsonParsed encoding is UNSTABLE** + Parsed-JSON encoding attempts to use program-specific state parsers to return more human-readable and explicit account state data. If parsed-JSON is requested but a parser cannot be found, the field falls back to base64 encoding, detectable when the `data` field is type ``. If parsed-JSON is requested for the SPL Token program, when a valid mint cannot be found for a particular account, that account will be filtered out from results. - (optional) `dataSlice: ` - limit the returned account data using the provided `offset: ` and `length: ` fields; only available for "base58" or "base64" encoding. - (optional) `filters: ` - filter results using various [filter objects](jsonrpc-api.md#filters); account must meet all filter criteria to be included in results @@ -2226,7 +2226,7 @@ Returns all SPL Token accounts by approved Delegate. **UNSTABLE** - `` - (optional) Configuration object containing the following optional fields: - (optional) [Commitment](jsonrpc-api.md#configuring-state-commitment) - `encoding: ` - encoding for Account data, either "base58" (*slow*), "base64" or jsonParsed". - Parsed-JSON encoding attempts to use program-specific state parsers to return more human-readable and explicit account state data. If parsed-JSON is requested but a valid mint cannot be found for a particular account, that account will be filtered out from results. **jsonParsed encoding is UNSTABLE** + Parsed-JSON encoding attempts to use program-specific state parsers to return more human-readable and explicit account state data. If parsed-JSON is requested but a valid mint cannot be found for a particular account, that account will be filtered out from results. - (optional) `dataSlice: ` - limit the returned account data using the provided `offset: ` and `length: ` fields; only available for "base58" or "base64" encoding. #### Results: @@ -2315,7 +2315,7 @@ Returns all SPL Token accounts by token owner. **UNSTABLE** - `` - (optional) Configuration object containing the following optional fields: - (optional) [Commitment](jsonrpc-api.md#configuring-state-commitment) - `encoding: ` - encoding for Account data, either "base58" (*slow*), "base64" or jsonParsed". - Parsed-JSON encoding attempts to use program-specific state parsers to return more human-readable and explicit account state data. If parsed-JSON is requested but a valid mint cannot be found for a particular account, that account will be filtered out from results. **jsonParsed encoding is UNSTABLE** + Parsed-JSON encoding attempts to use program-specific state parsers to return more human-readable and explicit account state data. If parsed-JSON is requested but a valid mint cannot be found for a particular account, that account will be filtered out from results. - (optional) `dataSlice: ` - limit the returned account data using the provided `offset: ` and `length: ` fields; only available for "base58" or "base64" encoding. #### Results: @@ -2824,7 +2824,7 @@ Subscribe to an account to receive notifications when the lamports or data for a - `` - (optional) Configuration object containing the following optional fields: - `` - (optional) [Commitment](jsonrpc-api.md#configuring-state-commitment) - `encoding: ` - encoding for Account data, either "base58" (*slow*), "base64" or jsonParsed". - Parsed-JSON encoding attempts to use program-specific state parsers to return more human-readable and explicit account state data. If parsed-JSON is requested but a parser cannot be found, the field falls back to binary encoding, detectable when the `data` field is type ``. **jsonParsed encoding is UNSTABLE** + Parsed-JSON encoding attempts to use program-specific state parsers to return more human-readable and explicit account state data. If parsed-JSON is requested but a parser cannot be found, the field falls back to binary encoding, detectable when the `data` field is type ``. #### Results: @@ -2959,7 +2959,7 @@ Subscribe to a program to receive notifications when the lamports or data for a - `` - (optional) Configuration object containing the following optional fields: - (optional) [Commitment](jsonrpc-api.md#configuring-state-commitment) - `encoding: ` - encoding for Account data, either "base58" (*slow*), "base64" or jsonParsed". - Parsed-JSON encoding attempts to use program-specific state parsers to return more human-readable and explicit account state data. If parsed-JSON is requested but a parser cannot be found, the field falls back to base64 encoding, detectable when the `data` field is type ``. **jsonParsed encoding is UNSTABLE** + Parsed-JSON encoding attempts to use program-specific state parsers to return more human-readable and explicit account state data. If parsed-JSON is requested but a parser cannot be found, the field falls back to base64 encoding, detectable when the `data` field is type ``. - (optional) `filters: ` - filter results using various [filter objects](jsonrpc-api.md#filters); account must meet all filter criteria to be included in results #### Results: diff --git a/sdk/src/system_instruction.rs b/sdk/src/system_instruction.rs index ab524a784..8682feee5 100644 --- a/sdk/src/system_instruction.rs +++ b/sdk/src/system_instruction.rs @@ -148,7 +148,8 @@ pub enum SystemInstruction { /// Change the entity authorized to execute nonce instructions on the account /// /// # Account references - /// 0. [WRITE, SIGNER] Nonce account + /// 0. [WRITE] Nonce account + /// 1. [SIGNER] Nonce authority /// /// The `Pubkey` parameter identifies the entity to authorize AuthorizeNonceAccount(Pubkey), diff --git a/transaction-status/src/lib.rs b/transaction-status/src/lib.rs index 7fbd7c374..383f9c30b 100644 --- a/transaction-status/src/lib.rs +++ b/transaction-status/src/lib.rs @@ -6,6 +6,8 @@ extern crate serde_derive; pub mod parse_accounts; pub mod parse_bpf_loader; pub mod parse_instruction; +pub mod parse_stake; +pub mod parse_system; pub mod parse_token; use crate::{ diff --git a/transaction-status/src/parse_instruction.rs b/transaction-status/src/parse_instruction.rs index 90e601949..6987e8e0b 100644 --- a/transaction-status/src/parse_instruction.rs +++ b/transaction-status/src/parse_instruction.rs @@ -1,8 +1,11 @@ -use crate::{parse_bpf_loader::parse_bpf_loader, parse_token::parse_token}; +use crate::{ + parse_bpf_loader::parse_bpf_loader, parse_stake::parse_stake, parse_system::parse_system, + parse_token::parse_token, +}; use inflector::Inflector; use serde_json::Value; use solana_account_decoder::parse_token::spl_token_id_v2_0; -use solana_sdk::{instruction::CompiledInstruction, pubkey::Pubkey}; +use solana_sdk::{instruction::CompiledInstruction, pubkey::Pubkey, system_program}; use std::{ collections::HashMap, str::{from_utf8, FromStr}, @@ -13,12 +16,16 @@ lazy_static! { static ref BPF_LOADER_PROGRAM_ID: Pubkey = solana_sdk::bpf_loader::id(); static ref MEMO_PROGRAM_ID: Pubkey = Pubkey::from_str(&spl_memo_v1_0::id().to_string()).unwrap(); + static ref STAKE_PROGRAM_ID: Pubkey = solana_stake_program::id(); + static ref SYSTEM_PROGRAM_ID: Pubkey = system_program::id(); static ref TOKEN_PROGRAM_ID: Pubkey = spl_token_id_v2_0(); static ref PARSABLE_PROGRAM_IDS: HashMap = { let mut m = HashMap::new(); m.insert(*MEMO_PROGRAM_ID, ParsableProgram::SplMemo); m.insert(*TOKEN_PROGRAM_ID, ParsableProgram::SplToken); m.insert(*BPF_LOADER_PROGRAM_ID, ParsableProgram::BpfLoader); + m.insert(*STAKE_PROGRAM_ID, ParsableProgram::Stake); + m.insert(*SYSTEM_PROGRAM_ID, ParsableProgram::System); m }; } @@ -61,6 +68,8 @@ pub enum ParsableProgram { SplMemo, SplToken, BpfLoader, + Stake, + System, } pub fn parse( @@ -77,6 +86,8 @@ pub fn parse( ParsableProgram::BpfLoader => { serde_json::to_value(parse_bpf_loader(instruction, account_keys)?)? } + ParsableProgram::Stake => serde_json::to_value(parse_stake(instruction, account_keys)?)?, + ParsableProgram::System => serde_json::to_value(parse_system(instruction, account_keys)?)?, }; Ok(ParsedInstruction { program: format!("{:?}", program_name).to_kebab_case(), @@ -89,6 +100,20 @@ fn parse_memo(instruction: &CompiledInstruction) -> Value { Value::String(from_utf8(&instruction.data).unwrap().to_string()) } +pub(crate) fn check_num_accounts( + accounts: &[u8], + num: usize, + parsable_program: ParsableProgram, +) -> Result<(), ParseInstructionError> { + if accounts.len() < num { + Err(ParseInstructionError::InstructionKeyMismatch( + parsable_program, + )) + } else { + Ok(()) + } +} + #[cfg(test)] mod test { use super::*; diff --git a/transaction-status/src/parse_stake.rs b/transaction-status/src/parse_stake.rs new file mode 100644 index 000000000..95fb31644 --- /dev/null +++ b/transaction-status/src/parse_stake.rs @@ -0,0 +1,452 @@ +use crate::parse_instruction::{ + check_num_accounts, ParsableProgram, ParseInstructionError, ParsedInstructionEnum, +}; +use bincode::deserialize; +use serde_json::{json, Map}; +use solana_sdk::{instruction::CompiledInstruction, pubkey::Pubkey}; +use solana_stake_program::stake_instruction::StakeInstruction; + +pub fn parse_stake( + instruction: &CompiledInstruction, + account_keys: &[Pubkey], +) -> Result { + let stake_instruction: StakeInstruction = deserialize(&instruction.data) + .map_err(|_| ParseInstructionError::InstructionNotParsable(ParsableProgram::Stake))?; + match instruction.accounts.iter().max() { + Some(index) if (*index as usize) < account_keys.len() => {} + _ => { + // Runtime should prevent this from ever happening + return Err(ParseInstructionError::InstructionKeyMismatch( + ParsableProgram::Stake, + )); + } + } + match stake_instruction { + StakeInstruction::Initialize(authorized, lockup) => { + check_num_stake_accounts(&instruction.accounts, 2)?; + let authorized = json!({ + "staker": authorized.staker.to_string(), + "withdrawer": authorized.withdrawer.to_string(), + }); + let lockup = json!({ + "unixTimestamp": lockup.unix_timestamp, + "epoch": lockup.epoch, + "custodian": lockup.custodian.to_string(), + }); + Ok(ParsedInstructionEnum { + instruction_type: "initialize".to_string(), + info: json!({ + "stakeAccount": account_keys[instruction.accounts[0] as usize].to_string(), + "rentSysvar": account_keys[instruction.accounts[1] as usize].to_string(), + "authorized": authorized, + "lockup": lockup, + }), + }) + } + StakeInstruction::Authorize(new_authorized, authority_type) => { + check_num_stake_accounts(&instruction.accounts, 3)?; + Ok(ParsedInstructionEnum { + instruction_type: "authorize".to_string(), + info: json!({ + "stakeAccount": account_keys[instruction.accounts[0] as usize].to_string(), + "clockSysvar": account_keys[instruction.accounts[1] as usize].to_string(), + "authority": account_keys[instruction.accounts[2] as usize].to_string(), + "newAuthority": new_authorized.to_string(), + "authorityType": authority_type, + }), + }) + } + StakeInstruction::DelegateStake => { + check_num_stake_accounts(&instruction.accounts, 6)?; + Ok(ParsedInstructionEnum { + instruction_type: "delegate".to_string(), + info: json!({ + "stakeAccount": account_keys[instruction.accounts[0] as usize].to_string(), + "voteAccount": account_keys[instruction.accounts[1] as usize].to_string(), + "clockSysvar": account_keys[instruction.accounts[2] as usize].to_string(), + "stakeHistorySysvar": account_keys[instruction.accounts[3] as usize].to_string(), + "stakeConfigAccount": account_keys[instruction.accounts[4] as usize].to_string(), + "stakeAuthority": account_keys[instruction.accounts[5] as usize].to_string(), + }), + }) + } + StakeInstruction::Split(lamports) => { + check_num_stake_accounts(&instruction.accounts, 3)?; + Ok(ParsedInstructionEnum { + instruction_type: "split".to_string(), + info: json!({ + "stakeAccount": account_keys[instruction.accounts[0] as usize].to_string(), + "newSplitAccount": account_keys[instruction.accounts[1] as usize].to_string(), + "stakeAuthority": account_keys[instruction.accounts[2] as usize].to_string(), + "lamports": lamports, + }), + }) + } + StakeInstruction::Withdraw(lamports) => { + check_num_stake_accounts(&instruction.accounts, 5)?; + let mut value = json!({ + "stakeAccount": account_keys[instruction.accounts[0] as usize].to_string(), + "destination": account_keys[instruction.accounts[1] as usize].to_string(), + "clockSysvar": account_keys[instruction.accounts[2] as usize].to_string(), + "stakeHistorySysvar": account_keys[instruction.accounts[3] as usize].to_string(), + "withdrawAuthority": account_keys[instruction.accounts[4] as usize].to_string(), + "lamports": lamports, + }); + let map = value.as_object_mut().unwrap(); + if instruction.accounts.len() == 6 { + map.insert( + "custodian".to_string(), + json!(account_keys[instruction.accounts[5] as usize].to_string()), + ); + } + Ok(ParsedInstructionEnum { + instruction_type: "withdraw".to_string(), + info: value, + }) + } + StakeInstruction::Deactivate => { + check_num_stake_accounts(&instruction.accounts, 3)?; + Ok(ParsedInstructionEnum { + instruction_type: "deactivate".to_string(), + info: json!({ + "stakeAccount": account_keys[instruction.accounts[0] as usize].to_string(), + "clockSysvar": account_keys[instruction.accounts[1] as usize].to_string(), + "stakeAuthority": account_keys[instruction.accounts[2] as usize].to_string(), + }), + }) + } + StakeInstruction::SetLockup(lockup_args) => { + check_num_stake_accounts(&instruction.accounts, 2)?; + let mut lockup_map = Map::new(); + if let Some(timestamp) = lockup_args.unix_timestamp { + lockup_map.insert("unixTimestamp".to_string(), json!(timestamp)); + } + if let Some(epoch) = lockup_args.epoch { + lockup_map.insert("epoch".to_string(), json!(epoch)); + } + if let Some(custodian) = lockup_args.custodian { + lockup_map.insert("custodian".to_string(), json!(custodian.to_string())); + } + Ok(ParsedInstructionEnum { + instruction_type: "setLockup".to_string(), + info: json!({ + "stakeAccount": account_keys[instruction.accounts[0] as usize].to_string(), + "custodian": account_keys[instruction.accounts[1] as usize].to_string(), + "lockup": lockup_map, + }), + }) + } + StakeInstruction::Merge => { + check_num_stake_accounts(&instruction.accounts, 5)?; + Ok(ParsedInstructionEnum { + instruction_type: "merge".to_string(), + info: json!({ + "destination": account_keys[instruction.accounts[0] as usize].to_string(), + "source": account_keys[instruction.accounts[1] as usize].to_string(), + "clockSysvar": account_keys[instruction.accounts[2] as usize].to_string(), + "stakeHistorySysvar": account_keys[instruction.accounts[3] as usize].to_string(), + "stakeAuthority": account_keys[instruction.accounts[4] as usize].to_string(), + }), + }) + } + StakeInstruction::AuthorizeWithSeed(args) => { + check_num_stake_accounts(&instruction.accounts, 2)?; + Ok(ParsedInstructionEnum { + instruction_type: "authorizeWithSeed".to_string(), + info: json!({ + "stakeAccount": account_keys[instruction.accounts[0] as usize].to_string(), + "authorityBase": account_keys[instruction.accounts[1] as usize].to_string(), + "newAuthorized": args.new_authorized_pubkey.to_string(), + "authorityType": args.stake_authorize, + "authoritySeed": args.authority_seed, + "authorityOwner": args.authority_owner.to_string(), + }), + }) + } + } +} + +fn check_num_stake_accounts(accounts: &[u8], num: usize) -> Result<(), ParseInstructionError> { + check_num_accounts(accounts, num, ParsableProgram::Stake) +} + +#[cfg(test)] +mod test { + use super::*; + use solana_sdk::{message::Message, pubkey::Pubkey}; + use solana_stake_program::{ + stake_instruction::{self, LockupArgs}, + stake_state::{Authorized, Lockup, StakeAuthorize}, + }; + + #[test] + #[allow(clippy::same_item_push)] + fn test_parse_stake_instruction() { + let mut keys: Vec = vec![]; + for _ in 0..6 { + keys.push(Pubkey::new_rand()); + } + + let authorized = Authorized { + staker: Pubkey::new_rand(), + withdrawer: Pubkey::new_rand(), + }; + let lockup = Lockup { + unix_timestamp: 1_234_567_890, + epoch: 11, + custodian: Pubkey::new_rand(), + }; + let lamports = 55; + + let instructions = + stake_instruction::create_account(&keys[0], &keys[1], &authorized, &lockup, lamports); + let message = Message::new(&instructions, None); + assert_eq!( + parse_stake(&message.instructions[1], &keys[0..3]).unwrap(), + ParsedInstructionEnum { + instruction_type: "initialize".to_string(), + info: json!({ + "stakeAccount": keys[1].to_string(), + "rentSysvar": keys[2].to_string(), + "authorized": { + "staker": authorized.staker.to_string(), + "withdrawer": authorized.withdrawer.to_string(), + }, + "lockup": { + "unixTimestamp": lockup.unix_timestamp, + "epoch": lockup.epoch, + "custodian": lockup.custodian.to_string(), + } + }), + } + ); + assert!(parse_stake(&message.instructions[1], &keys[0..2]).is_err()); + + let authority_type = StakeAuthorize::Staker; + let instruction = + stake_instruction::authorize(&keys[1], &keys[0], &keys[3], authority_type); + let message = Message::new(&[instruction], None); + assert_eq!( + parse_stake(&message.instructions[0], &keys[0..3]).unwrap(), + ParsedInstructionEnum { + instruction_type: "authorize".to_string(), + info: json!({ + "stakeAccount": keys[1].to_string(), + "clockSysvar": keys[2].to_string(), + "authority": keys[0].to_string(), + "newAuthority": keys[3].to_string(), + "authorityType": authority_type, + }), + } + ); + assert!(parse_stake(&message.instructions[0], &keys[0..2]).is_err()); + + let instruction = stake_instruction::delegate_stake(&keys[1], &keys[0], &keys[2]); + let message = Message::new(&[instruction], None); + assert_eq!( + parse_stake(&message.instructions[0], &keys[0..6]).unwrap(), + ParsedInstructionEnum { + instruction_type: "delegate".to_string(), + info: json!({ + "stakeAccount": keys[1].to_string(), + "voteAccount": keys[2].to_string(), + "clockSysvar": keys[3].to_string(), + "stakeHistorySysvar": keys[4].to_string(), + "stakeConfigAccount": keys[5].to_string(), + "stakeAuthority": keys[0].to_string(), + }), + } + ); + assert!(parse_stake(&message.instructions[0], &keys[0..5]).is_err()); + + let instructions = stake_instruction::split(&keys[2], &keys[0], lamports, &keys[1]); + let message = Message::new(&instructions, None); + assert_eq!( + parse_stake(&message.instructions[1], &keys[0..3]).unwrap(), + ParsedInstructionEnum { + instruction_type: "split".to_string(), + info: json!({ + "stakeAccount": keys[2].to_string(), + "newSplitAccount": keys[1].to_string(), + "stakeAuthority": keys[0].to_string(), + "lamports": lamports, + }), + } + ); + assert!(parse_stake(&message.instructions[1], &keys[0..2]).is_err()); + + let instruction = stake_instruction::withdraw(&keys[1], &keys[0], &keys[2], lamports, None); + let message = Message::new(&[instruction], None); + assert_eq!( + parse_stake(&message.instructions[0], &keys[0..5]).unwrap(), + ParsedInstructionEnum { + instruction_type: "withdraw".to_string(), + info: json!({ + "stakeAccount": keys[1].to_string(), + "destination": keys[2].to_string(), + "clockSysvar": keys[3].to_string(), + "stakeHistorySysvar": keys[4].to_string(), + "withdrawAuthority": keys[0].to_string(), + "lamports": lamports, + }), + } + ); + let instruction = + stake_instruction::withdraw(&keys[2], &keys[0], &keys[3], lamports, Some(&keys[1])); + let message = Message::new(&[instruction], None); + assert_eq!( + parse_stake(&message.instructions[0], &keys[0..6]).unwrap(), + ParsedInstructionEnum { + instruction_type: "withdraw".to_string(), + info: json!({ + "stakeAccount": keys[2].to_string(), + "destination": keys[3].to_string(), + "clockSysvar": keys[4].to_string(), + "stakeHistorySysvar": keys[5].to_string(), + "withdrawAuthority": keys[0].to_string(), + "custodian": keys[1].to_string(), + "lamports": lamports, + }), + } + ); + assert!(parse_stake(&message.instructions[0], &keys[0..4]).is_err()); + + let instruction = stake_instruction::deactivate_stake(&keys[1], &keys[0]); + let message = Message::new(&[instruction], None); + assert_eq!( + parse_stake(&message.instructions[0], &keys[0..3]).unwrap(), + ParsedInstructionEnum { + instruction_type: "deactivate".to_string(), + info: json!({ + "stakeAccount": keys[1].to_string(), + "clockSysvar": keys[2].to_string(), + "stakeAuthority": keys[0].to_string(), + }), + } + ); + assert!(parse_stake(&message.instructions[0], &keys[0..2]).is_err()); + + let instructions = stake_instruction::merge(&keys[1], &keys[0], &keys[2]); + let message = Message::new(&instructions, None); + assert_eq!( + parse_stake(&message.instructions[0], &keys[0..5]).unwrap(), + ParsedInstructionEnum { + instruction_type: "merge".to_string(), + info: json!({ + "destination": keys[1].to_string(), + "source": keys[2].to_string(), + "clockSysvar": keys[3].to_string(), + "stakeHistorySysvar": keys[4].to_string(), + "stakeAuthority": keys[0].to_string(), + }), + } + ); + assert!(parse_stake(&message.instructions[0], &keys[0..4]).is_err()); + + let seed = "test_seed"; + let instruction = stake_instruction::authorize_with_seed( + &keys[1], + &keys[0], + seed.to_string(), + &keys[2], + &keys[3], + authority_type, + ); + let message = Message::new(&[instruction], None); + assert_eq!( + parse_stake(&message.instructions[0], &keys[0..2]).unwrap(), + ParsedInstructionEnum { + instruction_type: "authorizeWithSeed".to_string(), + info: json!({ + "stakeAccount": keys[1].to_string(), + "authorityOwner": keys[2].to_string(), + "newAuthorized": keys[3].to_string(), + "authorityBase": keys[0].to_string(), + "authoritySeed": seed, + "authorityType": authority_type, + }), + } + ); + assert!(parse_stake(&message.instructions[0], &keys[0..1]).is_err()); + } + + #[test] + #[allow(clippy::same_item_push)] + fn test_parse_set_lockup() { + let mut keys: Vec = vec![]; + for _ in 0..2 { + keys.push(Pubkey::new_rand()); + } + let unix_timestamp = 1_234_567_890; + let epoch = 11; + let custodian = Pubkey::new_rand(); + + let lockup = LockupArgs { + unix_timestamp: Some(unix_timestamp), + epoch: None, + custodian: None, + }; + let instruction = stake_instruction::set_lockup(&keys[1], &lockup, &keys[0]); + let message = Message::new(&[instruction], None); + assert_eq!( + parse_stake(&message.instructions[0], &keys[0..2]).unwrap(), + ParsedInstructionEnum { + instruction_type: "setLockup".to_string(), + info: json!({ + "stakeAccount": keys[1].to_string(), + "custodian": keys[0].to_string(), + "lockup": { + "unixTimestamp": unix_timestamp + } + }), + } + ); + + let lockup = LockupArgs { + unix_timestamp: Some(unix_timestamp), + epoch: Some(epoch), + custodian: None, + }; + let instruction = stake_instruction::set_lockup(&keys[1], &lockup, &keys[0]); + let message = Message::new(&[instruction], None); + assert_eq!( + parse_stake(&message.instructions[0], &keys[0..2]).unwrap(), + ParsedInstructionEnum { + instruction_type: "setLockup".to_string(), + info: json!({ + "stakeAccount": keys[1].to_string(), + "custodian": keys[0].to_string(), + "lockup": { + "unixTimestamp": unix_timestamp, + "epoch": epoch, + } + }), + } + ); + + let lockup = LockupArgs { + unix_timestamp: Some(unix_timestamp), + epoch: Some(epoch), + custodian: Some(custodian), + }; + let instruction = stake_instruction::set_lockup(&keys[1], &lockup, &keys[0]); + let message = Message::new(&[instruction], None); + assert_eq!( + parse_stake(&message.instructions[0], &keys[0..2]).unwrap(), + ParsedInstructionEnum { + instruction_type: "setLockup".to_string(), + info: json!({ + "stakeAccount": keys[1].to_string(), + "custodian": keys[0].to_string(), + "lockup": { + "unixTimestamp": unix_timestamp, + "epoch": epoch, + "custodian": custodian.to_string(), + } + }), + } + ); + + assert!(parse_stake(&message.instructions[0], &keys[0..1]).is_err()); + } +} diff --git a/transaction-status/src/parse_system.rs b/transaction-status/src/parse_system.rs new file mode 100644 index 000000000..adab33347 --- /dev/null +++ b/transaction-status/src/parse_system.rs @@ -0,0 +1,432 @@ +use crate::parse_instruction::{ + check_num_accounts, ParsableProgram, ParseInstructionError, ParsedInstructionEnum, +}; +use bincode::deserialize; +use serde_json::json; +use solana_sdk::{ + instruction::CompiledInstruction, pubkey::Pubkey, system_instruction::SystemInstruction, +}; + +pub fn parse_system( + instruction: &CompiledInstruction, + account_keys: &[Pubkey], +) -> Result { + let system_instruction: SystemInstruction = deserialize(&instruction.data) + .map_err(|_| ParseInstructionError::InstructionNotParsable(ParsableProgram::System))?; + match instruction.accounts.iter().max() { + Some(index) if (*index as usize) < account_keys.len() => {} + _ => { + // Runtime should prevent this from ever happening + return Err(ParseInstructionError::InstructionKeyMismatch( + ParsableProgram::System, + )); + } + } + match system_instruction { + SystemInstruction::CreateAccount { + lamports, + space, + owner, + } => { + check_num_system_accounts(&instruction.accounts, 2)?; + Ok(ParsedInstructionEnum { + instruction_type: "createAccount".to_string(), + info: json!({ + "source": account_keys[instruction.accounts[0] as usize].to_string(), + "newAccount": account_keys[instruction.accounts[1] as usize].to_string(), + "lamports": lamports, + "space": space, + "owner": owner.to_string(), + }), + }) + } + SystemInstruction::Assign { owner } => { + check_num_system_accounts(&instruction.accounts, 1)?; + Ok(ParsedInstructionEnum { + instruction_type: "assign".to_string(), + info: json!({ + "account": account_keys[instruction.accounts[0] as usize].to_string(), + "owner": owner.to_string(), + }), + }) + } + SystemInstruction::Transfer { lamports } => { + check_num_system_accounts(&instruction.accounts, 2)?; + Ok(ParsedInstructionEnum { + instruction_type: "transfer".to_string(), + info: json!({ + "source": account_keys[instruction.accounts[0] as usize].to_string(), + "destination": account_keys[instruction.accounts[1] as usize].to_string(), + "lamports": lamports, + }), + }) + } + SystemInstruction::CreateAccountWithSeed { + base, + seed, + lamports, + space, + owner, + } => { + check_num_system_accounts(&instruction.accounts, 3)?; + Ok(ParsedInstructionEnum { + instruction_type: "createAccountWithSeed".to_string(), + info: json!({ + "source": account_keys[instruction.accounts[0] as usize].to_string(), + "newAccount": account_keys[instruction.accounts[2] as usize].to_string(), + "base": base.to_string(), + "seed": seed, + "lamports": lamports, + "space": space, + "owner": owner.to_string(), + }), + }) + } + SystemInstruction::AdvanceNonceAccount => { + check_num_system_accounts(&instruction.accounts, 3)?; + Ok(ParsedInstructionEnum { + instruction_type: "advanceNonce".to_string(), + info: json!({ + "nonceAccount": account_keys[instruction.accounts[0] as usize].to_string(), + "recentBlockhashesSysvar": account_keys[instruction.accounts[1] as usize].to_string(), + "nonceAuthority": account_keys[instruction.accounts[2] as usize].to_string(), + }), + }) + } + SystemInstruction::WithdrawNonceAccount(lamports) => { + check_num_system_accounts(&instruction.accounts, 5)?; + Ok(ParsedInstructionEnum { + instruction_type: "withdrawFromNonce".to_string(), + info: json!({ + "nonceAccount": account_keys[instruction.accounts[0] as usize].to_string(), + "destination": account_keys[instruction.accounts[1] as usize].to_string(), + "recentBlockhashesSysvar": account_keys[instruction.accounts[2] as usize].to_string(), + "rentSysvar": account_keys[instruction.accounts[3] as usize].to_string(), + "nonceAuthority": account_keys[instruction.accounts[4] as usize].to_string(), + "lamports": lamports, + }), + }) + } + SystemInstruction::InitializeNonceAccount(authority) => { + check_num_system_accounts(&instruction.accounts, 3)?; + Ok(ParsedInstructionEnum { + instruction_type: "initializeNonce".to_string(), + info: json!({ + "nonceAccount": account_keys[instruction.accounts[0] as usize].to_string(), + "recentBlockhashesSysvar": account_keys[instruction.accounts[1] as usize].to_string(), + "rentSysvar": account_keys[instruction.accounts[2] as usize].to_string(), + "nonceAuthority": authority.to_string(), + }), + }) + } + SystemInstruction::AuthorizeNonceAccount(authority) => { + check_num_system_accounts(&instruction.accounts, 1)?; + Ok(ParsedInstructionEnum { + instruction_type: "authorizeNonce".to_string(), + info: json!({ + "nonceAccount": account_keys[instruction.accounts[0] as usize].to_string(), + "nonceAuthority": account_keys[instruction.accounts[1] as usize].to_string(), + "newAuthorized": authority.to_string(), + }), + }) + } + SystemInstruction::Allocate { space } => { + check_num_system_accounts(&instruction.accounts, 1)?; + Ok(ParsedInstructionEnum { + instruction_type: "allocate".to_string(), + info: json!({ + "account": account_keys[instruction.accounts[0] as usize].to_string(), + "space": space, + }), + }) + } + SystemInstruction::AllocateWithSeed { + base, + seed, + space, + owner, + } => { + check_num_system_accounts(&instruction.accounts, 2)?; + Ok(ParsedInstructionEnum { + instruction_type: "allocateWithSeed".to_string(), + info: json!({ + "account": account_keys[instruction.accounts[0] as usize].to_string(), + "base": base.to_string(), + "seed": seed, + "space": space, + "owner": owner.to_string(), + }), + }) + } + SystemInstruction::AssignWithSeed { base, seed, owner } => { + check_num_system_accounts(&instruction.accounts, 2)?; + Ok(ParsedInstructionEnum { + instruction_type: "assignWithSeed".to_string(), + info: json!({ + "account": account_keys[instruction.accounts[0] as usize].to_string(), + "base": base.to_string(), + "seed": seed, + "owner": owner.to_string(), + }), + }) + } + SystemInstruction::TransferWithSeed { + lamports, + from_seed, + from_owner, + } => { + check_num_system_accounts(&instruction.accounts, 3)?; + Ok(ParsedInstructionEnum { + instruction_type: "transferWithSeed".to_string(), + info: json!({ + "source": account_keys[instruction.accounts[0] as usize].to_string(), + "sourceBase": account_keys[instruction.accounts[1] as usize].to_string(), + "destination": account_keys[instruction.accounts[2] as usize].to_string(), + "lamports": lamports, + "sourceSeed": from_seed, + "sourceOwner": from_owner.to_string(), + }), + }) + } + } +} + +fn check_num_system_accounts(accounts: &[u8], num: usize) -> Result<(), ParseInstructionError> { + check_num_accounts(accounts, num, ParsableProgram::System) +} + +#[cfg(test)] +mod test { + use super::*; + use solana_sdk::{message::Message, pubkey::Pubkey, system_instruction}; + + #[test] + #[allow(clippy::same_item_push)] + fn test_parse_system_instruction() { + let mut keys: Vec = vec![]; + for _ in 0..6 { + keys.push(Pubkey::new_rand()); + } + + let lamports = 55; + let space = 128; + + let instruction = + system_instruction::create_account(&keys[0], &keys[1], lamports, space, &keys[2]); + let message = Message::new(&[instruction], None); + assert_eq!( + parse_system(&message.instructions[0], &keys[0..2]).unwrap(), + ParsedInstructionEnum { + instruction_type: "createAccount".to_string(), + info: json!({ + "source": keys[0].to_string(), + "newAccount": keys[1].to_string(), + "lamports": lamports, + "owner": keys[2].to_string(), + "space": space, + }), + } + ); + assert!(parse_system(&message.instructions[0], &keys[0..1]).is_err()); + + let instruction = system_instruction::assign(&keys[0], &keys[1]); + let message = Message::new(&[instruction], None); + assert_eq!( + parse_system(&message.instructions[0], &keys[0..1]).unwrap(), + ParsedInstructionEnum { + instruction_type: "assign".to_string(), + info: json!({ + "account": keys[0].to_string(), + "owner": keys[1].to_string(), + }), + } + ); + assert!(parse_system(&message.instructions[0], &[]).is_err()); + + let instruction = system_instruction::transfer(&keys[0], &keys[1], lamports); + let message = Message::new(&[instruction], None); + assert_eq!( + parse_system(&message.instructions[0], &keys[0..2]).unwrap(), + ParsedInstructionEnum { + instruction_type: "transfer".to_string(), + info: json!({ + "source": keys[0].to_string(), + "destination": keys[1].to_string(), + "lamports": lamports, + }), + } + ); + assert!(parse_system(&message.instructions[0], &keys[0..1]).is_err()); + + let seed = "test_seed"; + let instruction = system_instruction::create_account_with_seed( + &keys[0], &keys[1], &keys[2], seed, lamports, space, &keys[3], + ); + let message = Message::new(&[instruction], None); + assert_eq!( + parse_system(&message.instructions[0], &keys[0..3]).unwrap(), + ParsedInstructionEnum { + instruction_type: "createAccountWithSeed".to_string(), + info: json!({ + "source": keys[0].to_string(), + "newAccount": keys[1].to_string(), + "lamports": lamports, + "base": keys[2].to_string(), + "seed": seed, + "owner": keys[3].to_string(), + "space": space, + }), + } + ); + assert!(parse_system(&message.instructions[0], &keys[0..2]).is_err()); + + let instruction = system_instruction::allocate(&keys[0], space); + let message = Message::new(&[instruction], None); + assert_eq!( + parse_system(&message.instructions[0], &keys[0..1]).unwrap(), + ParsedInstructionEnum { + instruction_type: "allocate".to_string(), + info: json!({ + "account": keys[0].to_string(), + "space": space, + }), + } + ); + assert!(parse_system(&message.instructions[0], &[]).is_err()); + + let instruction = + system_instruction::allocate_with_seed(&keys[1], &keys[0], seed, space, &keys[2]); + let message = Message::new(&[instruction], None); + assert_eq!( + parse_system(&message.instructions[0], &keys[0..2]).unwrap(), + ParsedInstructionEnum { + instruction_type: "allocateWithSeed".to_string(), + info: json!({ + "account": keys[1].to_string(), + "base": keys[0].to_string(), + "seed": seed, + "owner": keys[2].to_string(), + "space": space, + }), + } + ); + assert!(parse_system(&message.instructions[0], &keys[0..1]).is_err()); + + let instruction = system_instruction::assign_with_seed(&keys[1], &keys[0], seed, &keys[2]); + let message = Message::new(&[instruction], None); + assert_eq!( + parse_system(&message.instructions[0], &keys[0..2]).unwrap(), + ParsedInstructionEnum { + instruction_type: "assignWithSeed".to_string(), + info: json!({ + "account": keys[1].to_string(), + "base": keys[0].to_string(), + "seed": seed, + "owner": keys[2].to_string(), + }), + } + ); + assert!(parse_system(&message.instructions[0], &keys[0..1]).is_err()); + + let instruction = system_instruction::transfer_with_seed( + &keys[1], + &keys[0], + seed.to_string(), + &keys[3], + &keys[2], + lamports, + ); + let message = Message::new(&[instruction], None); + assert_eq!( + parse_system(&message.instructions[0], &keys[0..3]).unwrap(), + ParsedInstructionEnum { + instruction_type: "transferWithSeed".to_string(), + info: json!({ + "source": keys[1].to_string(), + "sourceBase": keys[0].to_string(), + "sourceSeed": seed, + "sourceOwner": keys[3].to_string(), + "lamports": lamports, + "destination": keys[2].to_string() + }), + } + ); + assert!(parse_system(&message.instructions[0], &keys[0..2]).is_err()); + } + + #[test] + #[allow(clippy::same_item_push)] + fn test_parse_system_instruction_nonce() { + let mut keys: Vec = vec![]; + for _ in 0..5 { + keys.push(Pubkey::new_rand()); + } + + let instruction = system_instruction::advance_nonce_account(&keys[1], &keys[0]); + let message = Message::new(&[instruction], None); + assert_eq!( + parse_system(&message.instructions[0], &keys[0..3]).unwrap(), + ParsedInstructionEnum { + instruction_type: "advanceNonce".to_string(), + info: json!({ + "nonceAccount": keys[1].to_string(), + "recentBlockhashesSysvar": keys[2].to_string(), + "nonceAuthority": keys[0].to_string(), + }), + } + ); + assert!(parse_system(&message.instructions[0], &keys[0..2]).is_err()); + + let lamports = 55; + let instruction = + system_instruction::withdraw_nonce_account(&keys[1], &keys[0], &keys[2], lamports); + let message = Message::new(&[instruction], None); + assert_eq!( + parse_system(&message.instructions[0], &keys[0..5]).unwrap(), + ParsedInstructionEnum { + instruction_type: "withdrawFromNonce".to_string(), + info: json!({ + "nonceAccount": keys[1].to_string(), + "destination": keys[2].to_string(), + "recentBlockhashesSysvar": keys[3].to_string(), + "rentSysvar": keys[4].to_string(), + "nonceAuthority": keys[0].to_string(), + "lamports": lamports + }), + } + ); + assert!(parse_system(&message.instructions[0], &keys[0..4]).is_err()); + + let instructions = + system_instruction::create_nonce_account(&keys[0], &keys[1], &keys[4], lamports); + let message = Message::new(&instructions, None); + assert_eq!( + parse_system(&message.instructions[1], &keys[0..4]).unwrap(), + ParsedInstructionEnum { + instruction_type: "initializeNonce".to_string(), + info: json!({ + "nonceAccount": keys[1].to_string(), + "recentBlockhashesSysvar": keys[2].to_string(), + "rentSysvar": keys[3].to_string(), + "nonceAuthority": keys[4].to_string(), + }), + } + ); + assert!(parse_system(&message.instructions[1], &keys[0..3]).is_err()); + + let instruction = system_instruction::authorize_nonce_account(&keys[1], &keys[0], &keys[2]); + let message = Message::new(&[instruction], None); + assert_eq!( + parse_system(&message.instructions[0], &keys[0..2]).unwrap(), + ParsedInstructionEnum { + instruction_type: "authorizeNonce".to_string(), + info: json!({ + "nonceAccount": keys[1].to_string(), + "newAuthorized": keys[2].to_string(), + "nonceAuthority": keys[0].to_string(), + }), + } + ); + assert!(parse_system(&message.instructions[0], &keys[0..1]).is_err()); + } +} diff --git a/transaction-status/src/parse_token.rs b/transaction-status/src/parse_token.rs index d1184ad9f..19379aebd 100644 --- a/transaction-status/src/parse_token.rs +++ b/transaction-status/src/parse_token.rs @@ -1,4 +1,6 @@ -use crate::parse_instruction::{ParsableProgram, ParseInstructionError, ParsedInstructionEnum}; +use crate::parse_instruction::{ + check_num_accounts, ParsableProgram, ParseInstructionError, ParsedInstructionEnum, +}; use serde_json::{json, Map, Value}; use solana_account_decoder::parse_token::token_amount_to_ui_amount; use solana_sdk::{instruction::CompiledInstruction, pubkey::Pubkey}; @@ -13,11 +15,14 @@ pub fn parse_token( ) -> Result { let token_instruction = TokenInstruction::unpack(&instruction.data) .map_err(|_| ParseInstructionError::InstructionNotParsable(ParsableProgram::SplToken))?; - if instruction.accounts.len() > account_keys.len() { - // Runtime should prevent this from ever happening - return Err(ParseInstructionError::InstructionKeyMismatch( - ParsableProgram::SplToken, - )); + match instruction.accounts.iter().max() { + Some(index) if (*index as usize) < account_keys.len() => {} + _ => { + // Runtime should prevent this from ever happening + return Err(ParseInstructionError::InstructionKeyMismatch( + ParsableProgram::SplToken, + )); + } } match token_instruction { TokenInstruction::InitializeMint { @@ -25,11 +30,7 @@ pub fn parse_token( mint_authority, freeze_authority, } => { - if instruction.accounts.len() < 2 { - return Err(ParseInstructionError::InstructionKeyMismatch( - ParsableProgram::SplToken, - )); - } + check_num_token_accounts(&instruction.accounts, 2)?; let mut value = json!({ "mint": account_keys[instruction.accounts[0] as usize].to_string(), "decimals": decimals, @@ -49,11 +50,7 @@ pub fn parse_token( }) } TokenInstruction::InitializeAccount => { - if instruction.accounts.len() < 4 { - return Err(ParseInstructionError::InstructionKeyMismatch( - ParsableProgram::SplToken, - )); - } + check_num_token_accounts(&instruction.accounts, 4)?; Ok(ParsedInstructionEnum { instruction_type: "initializeAccount".to_string(), info: json!({ @@ -65,11 +62,7 @@ pub fn parse_token( }) } TokenInstruction::InitializeMultisig { m } => { - if instruction.accounts.len() < 3 { - return Err(ParseInstructionError::InstructionKeyMismatch( - ParsableProgram::SplToken, - )); - } + check_num_token_accounts(&instruction.accounts, 3)?; let mut signers: Vec = vec![]; for i in instruction.accounts[2..].iter() { signers.push(account_keys[*i as usize].to_string()); @@ -85,11 +78,7 @@ pub fn parse_token( }) } TokenInstruction::Transfer { amount } => { - if instruction.accounts.len() < 3 { - return Err(ParseInstructionError::InstructionKeyMismatch( - ParsableProgram::SplToken, - )); - } + check_num_token_accounts(&instruction.accounts, 3)?; let mut value = json!({ "source": account_keys[instruction.accounts[0] as usize].to_string(), "destination": account_keys[instruction.accounts[1] as usize].to_string(), @@ -110,11 +99,7 @@ pub fn parse_token( }) } TokenInstruction::Approve { amount } => { - if instruction.accounts.len() < 3 { - return Err(ParseInstructionError::InstructionKeyMismatch( - ParsableProgram::SplToken, - )); - } + check_num_token_accounts(&instruction.accounts, 3)?; let mut value = json!({ "source": account_keys[instruction.accounts[0] as usize].to_string(), "delegate": account_keys[instruction.accounts[1] as usize].to_string(), @@ -135,11 +120,7 @@ pub fn parse_token( }) } TokenInstruction::Revoke => { - if instruction.accounts.len() < 2 { - return Err(ParseInstructionError::InstructionKeyMismatch( - ParsableProgram::SplToken, - )); - } + check_num_token_accounts(&instruction.accounts, 2)?; let mut value = json!({ "source": account_keys[instruction.accounts[0] as usize].to_string(), }); @@ -161,11 +142,7 @@ pub fn parse_token( authority_type, new_authority, } => { - if instruction.accounts.len() < 2 { - return Err(ParseInstructionError::InstructionKeyMismatch( - ParsableProgram::SplToken, - )); - } + check_num_token_accounts(&instruction.accounts, 2)?; let owned = match authority_type { AuthorityType::MintTokens | AuthorityType::FreezeAccount => "mint", AuthorityType::AccountOwner | AuthorityType::CloseAccount => "account", @@ -193,11 +170,7 @@ pub fn parse_token( }) } TokenInstruction::MintTo { amount } => { - if instruction.accounts.len() < 3 { - return Err(ParseInstructionError::InstructionKeyMismatch( - ParsableProgram::SplToken, - )); - } + check_num_token_accounts(&instruction.accounts, 3)?; let mut value = json!({ "mint": account_keys[instruction.accounts[0] as usize].to_string(), "account": account_keys[instruction.accounts[1] as usize].to_string(), @@ -218,11 +191,7 @@ pub fn parse_token( }) } TokenInstruction::Burn { amount } => { - if instruction.accounts.len() < 3 { - return Err(ParseInstructionError::InstructionKeyMismatch( - ParsableProgram::SplToken, - )); - } + check_num_token_accounts(&instruction.accounts, 3)?; let mut value = json!({ "account": account_keys[instruction.accounts[0] as usize].to_string(), "mint": account_keys[instruction.accounts[1] as usize].to_string(), @@ -243,11 +212,7 @@ pub fn parse_token( }) } TokenInstruction::CloseAccount => { - if instruction.accounts.len() < 3 { - return Err(ParseInstructionError::InstructionKeyMismatch( - ParsableProgram::SplToken, - )); - } + check_num_token_accounts(&instruction.accounts, 3)?; let mut value = json!({ "account": account_keys[instruction.accounts[0] as usize].to_string(), "destination": account_keys[instruction.accounts[1] as usize].to_string(), @@ -267,11 +232,7 @@ pub fn parse_token( }) } TokenInstruction::FreezeAccount => { - if instruction.accounts.len() < 3 { - return Err(ParseInstructionError::InstructionKeyMismatch( - ParsableProgram::SplToken, - )); - } + check_num_token_accounts(&instruction.accounts, 3)?; let mut value = json!({ "account": account_keys[instruction.accounts[0] as usize].to_string(), "mint": account_keys[instruction.accounts[1] as usize].to_string(), @@ -291,11 +252,7 @@ pub fn parse_token( }) } TokenInstruction::ThawAccount => { - if instruction.accounts.len() < 3 { - return Err(ParseInstructionError::InstructionKeyMismatch( - ParsableProgram::SplToken, - )); - } + check_num_token_accounts(&instruction.accounts, 3)?; let mut value = json!({ "account": account_keys[instruction.accounts[0] as usize].to_string(), "mint": account_keys[instruction.accounts[1] as usize].to_string(), @@ -315,11 +272,7 @@ pub fn parse_token( }) } TokenInstruction::TransferChecked { amount, decimals } => { - if instruction.accounts.len() < 4 { - return Err(ParseInstructionError::InstructionKeyMismatch( - ParsableProgram::SplToken, - )); - } + check_num_token_accounts(&instruction.accounts, 4)?; let mut value = json!({ "source": account_keys[instruction.accounts[0] as usize].to_string(), "mint": account_keys[instruction.accounts[1] as usize].to_string(), @@ -341,11 +294,7 @@ pub fn parse_token( }) } TokenInstruction::ApproveChecked { amount, decimals } => { - if instruction.accounts.len() < 4 { - return Err(ParseInstructionError::InstructionKeyMismatch( - ParsableProgram::SplToken, - )); - } + check_num_token_accounts(&instruction.accounts, 4)?; let mut value = json!({ "source": account_keys[instruction.accounts[0] as usize].to_string(), "mint": account_keys[instruction.accounts[1] as usize].to_string(), @@ -367,11 +316,7 @@ pub fn parse_token( }) } TokenInstruction::MintToChecked { amount, decimals } => { - if instruction.accounts.len() < 3 { - return Err(ParseInstructionError::InstructionKeyMismatch( - ParsableProgram::SplToken, - )); - } + check_num_token_accounts(&instruction.accounts, 3)?; let mut value = json!({ "mint": account_keys[instruction.accounts[0] as usize].to_string(), "account": account_keys[instruction.accounts[1] as usize].to_string(), @@ -392,11 +337,7 @@ pub fn parse_token( }) } TokenInstruction::BurnChecked { amount, decimals } => { - if instruction.accounts.len() < 3 { - return Err(ParseInstructionError::InstructionKeyMismatch( - ParsableProgram::SplToken, - )); - } + check_num_token_accounts(&instruction.accounts, 3)?; let mut value = json!({ "account": account_keys[instruction.accounts[0] as usize].to_string(), "mint": account_keys[instruction.accounts[1] as usize].to_string(), @@ -465,6 +406,10 @@ fn parse_signers( } } +fn check_num_token_accounts(accounts: &[u8], num: usize) -> Result<(), ParseInstructionError> { + check_num_accounts(accounts, num, ParsableProgram::SplToken) +} + #[cfg(test)] mod test { use super::*;