diff --git a/Cargo.lock b/Cargo.lock index ef2aa5cf38..18b9e459b3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4881,8 +4881,11 @@ dependencies = [ "serde", "serde_derive", "serde_json", + "solana-account-decoder", "solana-sdk 1.2.16", "spl-memo", + "spl-token", + "thiserror", ] [[package]] diff --git a/account-decoder/src/parse_account_data.rs b/account-decoder/src/parse_account_data.rs index 07beeb7b93..e10743cadf 100644 --- a/account-decoder/src/parse_account_data.rs +++ b/account-decoder/src/parse_account_data.rs @@ -16,7 +16,7 @@ lazy_static! { pub static ref PARSABLE_PROGRAM_IDS: HashMap = { let mut m = HashMap::new(); m.insert(*SYSTEM_PROGRAM_ID, ParsableAccount::Nonce); - m.insert(*TOKEN_PROGRAM_ID, ParsableAccount::Token); + m.insert(*TOKEN_PROGRAM_ID, ParsableAccount::SplToken); m.insert(*VOTE_PROGRAM_ID, ParsableAccount::Vote); m }; @@ -41,7 +41,7 @@ pub enum ParseAccountError { #[serde(rename_all = "camelCase")] pub enum ParsableAccount { Nonce, - Token, + SplToken, Vote, } @@ -51,7 +51,7 @@ pub fn parse_account_data(program_id: &Pubkey, data: &[u8]) -> Result serde_json::to_value(parse_nonce(data)?)?, - ParsableAccount::Token => serde_json::to_value(parse_token(data)?)?, + ParsableAccount::SplToken => serde_json::to_value(parse_token(data)?)?, ParsableAccount::Vote => serde_json::to_value(parse_vote(data)?)?, }; Ok(json!({ diff --git a/account-decoder/src/parse_token.rs b/account-decoder/src/parse_token.rs index a7772c1bea..5b4a777115 100644 --- a/account-decoder/src/parse_token.rs +++ b/account-decoder/src/parse_token.rs @@ -16,7 +16,7 @@ pub fn parse_token(data: &[u8]) -> Result { let mut data = data.to_vec(); if data.len() == size_of::() { let account: Account = *State::unpack(&mut data) - .map_err(|_| ParseAccountError::AccountNotParsable(ParsableAccount::Token))?; + .map_err(|_| ParseAccountError::AccountNotParsable(ParsableAccount::SplToken))?; Ok(TokenAccountType::Account(UiTokenAccount { mint: account.mint.to_string(), owner: account.owner.to_string(), @@ -31,7 +31,7 @@ pub fn parse_token(data: &[u8]) -> Result { })) } else if data.len() == size_of::() { let mint: Mint = *State::unpack(&mut data) - .map_err(|_| ParseAccountError::AccountNotParsable(ParsableAccount::Token))?; + .map_err(|_| ParseAccountError::AccountNotParsable(ParsableAccount::SplToken))?; Ok(TokenAccountType::Mint(UiMint { owner: match mint.owner { COption::Some(pubkey) => Some(pubkey.to_string()), @@ -42,7 +42,7 @@ pub fn parse_token(data: &[u8]) -> Result { })) } else if data.len() == size_of::() { let multisig: Multisig = *State::unpack(&mut data) - .map_err(|_| ParseAccountError::AccountNotParsable(ParsableAccount::Token))?; + .map_err(|_| ParseAccountError::AccountNotParsable(ParsableAccount::SplToken))?; Ok(TokenAccountType::Multisig(UiMultisig { num_required_signers: multisig.m, num_valid_signers: multisig.n, @@ -61,7 +61,7 @@ pub fn parse_token(data: &[u8]) -> Result { })) } else { Err(ParseAccountError::AccountNotParsable( - ParsableAccount::Token, + ParsableAccount::SplToken, )) } } diff --git a/transaction-status/Cargo.toml b/transaction-status/Cargo.toml index 26f79747c8..81b1be71e8 100644 --- a/transaction-status/Cargo.toml +++ b/transaction-status/Cargo.toml @@ -13,11 +13,14 @@ bincode = "1.2.1" bs58 = "0.3.1" Inflector = "0.11.4" lazy_static = "1.4.0" +solana-account-decoder = { path = "../account-decoder", version = "1.2.16" } solana-sdk = { path = "../sdk", version = "1.2.16" } spl-memo-v1-0 = { package = "spl-memo", version = "1.0.4", features = ["skip-no-mangle"] } +spl-token-v1-0 = { package = "spl-token", version = "1.0.3", features = ["skip-no-mangle"] } serde = "1.0.110" serde_derive = "1.0.103" serde_json = "1.0.54" +thiserror = "1.0" [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] diff --git a/transaction-status/src/lib.rs b/transaction-status/src/lib.rs index 20635d7615..26f94c6259 100644 --- a/transaction-status/src/lib.rs +++ b/transaction-status/src/lib.rs @@ -5,6 +5,7 @@ extern crate serde_derive; pub mod parse_accounts; pub mod parse_instruction; +pub mod parse_token; use crate::{parse_accounts::parse_accounts, parse_instruction::parse}; use serde_json::Value; @@ -220,7 +221,11 @@ impl EncodedTransaction { .map(|instruction| { let program_id = instruction.program_id(&transaction.message.account_keys); - if let Some(parsed_instruction) = parse(program_id, instruction) { + if let Ok(parsed_instruction) = parse( + program_id, + instruction, + &transaction.message.account_keys, + ) { UiInstruction::Parsed(parsed_instruction) } else { UiInstruction::Compiled(instruction.into()) diff --git a/transaction-status/src/parse_instruction.rs b/transaction-status/src/parse_instruction.rs index e6264b4f33..a08d92afae 100644 --- a/transaction-status/src/parse_instruction.rs +++ b/transaction-status/src/parse_instruction.rs @@ -1,34 +1,60 @@ +use crate::parse_token::parse_token; use inflector::Inflector; use serde_json::{json, Value}; +use solana_account_decoder::parse_token::spl_token_id_v1_0; use solana_sdk::{instruction::CompiledInstruction, pubkey::Pubkey}; use std::{ collections::HashMap, str::{from_utf8, FromStr}, }; +use thiserror::Error; lazy_static! { static ref MEMO_PROGRAM_ID: Pubkey = Pubkey::from_str(&spl_memo_v1_0::id().to_string()).unwrap(); + static ref TOKEN_PROGRAM_ID: Pubkey = spl_token_id_v1_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 }; } -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -enum ParsableProgram { - SplMemo, +#[derive(Error, Debug, PartialEq)] +pub enum ParseInstructionError { + #[error("{0:?} instruction not parsable")] + InstructionNotParsable(ParsableProgram), + + #[error("{0:?} instruction key mismatch")] + InstructionKeyMismatch(ParsableProgram), + + #[error("Program not parsable")] + ProgramNotParsable, } -pub fn parse(program_id: &Pubkey, instruction: &CompiledInstruction) -> Option { - PARSABLE_PROGRAM_IDS.get(program_id).map(|program_name| { - let parsed_json = match program_name { - ParsableProgram::SplMemo => parse_memo(instruction), - }; - json!({ format!("{:?}", program_name).to_kebab_case(): parsed_json }) - }) +#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub enum ParsableProgram { + SplMemo, + SplToken, +} + +pub fn parse( + program_id: &Pubkey, + instruction: &CompiledInstruction, + account_keys: &[Pubkey], +) -> Result { + let program_name = PARSABLE_PROGRAM_IDS + .get(program_id) + .ok_or_else(|| ParseInstructionError::ProgramNotParsable)?; + let parsed_json = match program_name { + ParsableProgram::SplMemo => parse_memo(instruction), + ParsableProgram::SplToken => parse_token(instruction, account_keys)?, + }; + Ok(json!({ + format!("{:?}", program_name).to_kebab_case(): parsed_json + })) } fn parse_memo(instruction: &CompiledInstruction) -> Value { @@ -50,11 +76,14 @@ mod test { "spl-memo": "🦖" }); assert_eq!( - parse(&MEMO_PROGRAM_ID, &memo_instruction), - Some(expected_json) + parse(&MEMO_PROGRAM_ID, &memo_instruction, &[]).unwrap(), + expected_json ); let non_parsable_program_id = Pubkey::new(&[1; 32]); - assert_eq!(parse(&non_parsable_program_id, &memo_instruction), None); + assert_eq!( + parse(&non_parsable_program_id, &memo_instruction, &[]).unwrap_err(), + ParseInstructionError::ProgramNotParsable + ); } } diff --git a/transaction-status/src/parse_token.rs b/transaction-status/src/parse_token.rs new file mode 100644 index 0000000000..92f0f99f7b --- /dev/null +++ b/transaction-status/src/parse_token.rs @@ -0,0 +1,846 @@ +use crate::parse_instruction::{ParsableProgram, ParseInstructionError}; +use serde_json::{json, Map, Value}; +use solana_sdk::{instruction::CompiledInstruction, pubkey::Pubkey}; +use spl_token_v1_0::instruction::TokenInstruction; + +pub fn parse_token( + instruction: &CompiledInstruction, + account_keys: &[Pubkey], +) -> 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 token_instruction { + TokenInstruction::InitializeMint { amount, decimals } => { + if instruction.accounts.len() < 2 { + return Err(ParseInstructionError::InstructionKeyMismatch( + ParsableProgram::SplToken, + )); + } + let mut value = json!({ + "type": "initializeMint", + "mint": account_keys[instruction.accounts[0] as usize].to_string(), + "amount": amount, + "decimals":decimals, + }); + let map = value.as_object_mut().unwrap(); + if amount == 0 { + map.insert( + "owner".to_string(), + json!(account_keys[instruction.accounts[1] as usize].to_string()), + ); + } else { + map.insert( + "account".to_string(), + json!(account_keys[instruction.accounts[1] as usize].to_string()), + ); + if let Some(i) = instruction.accounts.get(2) { + map.insert( + "owner".to_string(), + json!(account_keys[*i as usize].to_string()), + ); + } + } + Ok(value) + } + TokenInstruction::InitializeAccount => { + if instruction.accounts.len() < 3 { + return Err(ParseInstructionError::InstructionKeyMismatch( + ParsableProgram::SplToken, + )); + } + Ok(json!({ + "type": "initializeAccount", + "account": account_keys[instruction.accounts[0] as usize].to_string(), + "mint": account_keys[instruction.accounts[1] as usize].to_string(), + "owner": account_keys[instruction.accounts[2] as usize].to_string(), + })) + } + TokenInstruction::InitializeMultisig { m } => { + if instruction.accounts.len() < 2 { + return Err(ParseInstructionError::InstructionKeyMismatch( + ParsableProgram::SplToken, + )); + } + let mut signers: Vec = vec![]; + for i in instruction.accounts[1..].iter() { + signers.push(account_keys[*i as usize].to_string()); + } + Ok(json!({ + "type": "initializeMultisig", + "multisig": account_keys[instruction.accounts[0] as usize].to_string(), + "signers": signers, + "m": m, + })) + } + TokenInstruction::Transfer { amount } => { + if instruction.accounts.len() < 3 { + return Err(ParseInstructionError::InstructionKeyMismatch( + ParsableProgram::SplToken, + )); + } + let mut value = json!({ + "type": "transfer", + "source": account_keys[instruction.accounts[0] as usize].to_string(), + "destination": account_keys[instruction.accounts[1] as usize].to_string(), + "amount": amount, + }); + let mut map = value.as_object_mut().unwrap(); + parse_signers( + &mut map, + 2, + account_keys, + &instruction.accounts, + "authority", + "multisigAuthority", + ); + Ok(value) + } + TokenInstruction::Approve { amount } => { + if instruction.accounts.len() < 3 { + return Err(ParseInstructionError::InstructionKeyMismatch( + ParsableProgram::SplToken, + )); + } + let mut value = json!({ + "type": "approve", + "source": account_keys[instruction.accounts[0] as usize].to_string(), + "delegate": account_keys[instruction.accounts[1] as usize].to_string(), + "amount": amount, + }); + let mut map = value.as_object_mut().unwrap(); + parse_signers( + &mut map, + 2, + account_keys, + &instruction.accounts, + "owner", + "multisigOwner", + ); + Ok(value) + } + TokenInstruction::Revoke => { + if instruction.accounts.len() < 2 { + return Err(ParseInstructionError::InstructionKeyMismatch( + ParsableProgram::SplToken, + )); + } + let mut value = json!({ + "type": "revoke", + "source": account_keys[instruction.accounts[0] as usize].to_string(), + }); + let mut map = value.as_object_mut().unwrap(); + parse_signers( + &mut map, + 1, + account_keys, + &instruction.accounts, + "owner", + "multisigOwner", + ); + Ok(value) + } + TokenInstruction::SetOwner => { + if instruction.accounts.len() < 3 { + return Err(ParseInstructionError::InstructionKeyMismatch( + ParsableProgram::SplToken, + )); + } + let mut value = json!({ + "type": "setOwner", + "owned": account_keys[instruction.accounts[0] as usize].to_string(), + "newOwner": account_keys[instruction.accounts[1] as usize].to_string(), + }); + let mut map = value.as_object_mut().unwrap(); + parse_signers( + &mut map, + 2, + account_keys, + &instruction.accounts, + "owner", + "multisigOwner", + ); + Ok(value) + } + TokenInstruction::MintTo { amount } => { + if instruction.accounts.len() < 3 { + return Err(ParseInstructionError::InstructionKeyMismatch( + ParsableProgram::SplToken, + )); + } + let mut value = json!({ + "type": "mintTo", + "mint": account_keys[instruction.accounts[0] as usize].to_string(), + "account": account_keys[instruction.accounts[1] as usize].to_string(), + "amount": amount, + }); + let mut map = value.as_object_mut().unwrap(); + parse_signers( + &mut map, + 2, + account_keys, + &instruction.accounts, + "owner", + "multisigOwner", + ); + Ok(value) + } + TokenInstruction::Burn { amount } => { + if instruction.accounts.len() < 2 { + return Err(ParseInstructionError::InstructionKeyMismatch( + ParsableProgram::SplToken, + )); + } + let mut value = json!({ + "type": "burn", + "account": account_keys[instruction.accounts[0] as usize].to_string(), + "amount": amount, + }); + let mut map = value.as_object_mut().unwrap(); + parse_signers( + &mut map, + 1, + account_keys, + &instruction.accounts, + "authority", + "multisigAuthority", + ); + Ok(value) + } + TokenInstruction::CloseAccount => { + if instruction.accounts.len() < 3 { + return Err(ParseInstructionError::InstructionKeyMismatch( + ParsableProgram::SplToken, + )); + } + let mut value = json!({ + "type": "closeAccount", + "account": account_keys[instruction.accounts[0] as usize].to_string(), + "destination": account_keys[instruction.accounts[1] as usize].to_string(), + }); + let mut map = value.as_object_mut().unwrap(); + parse_signers( + &mut map, + 2, + account_keys, + &instruction.accounts, + "owner", + "multisigOwner", + ); + Ok(value) + } + } +} + +fn parse_signers( + map: &mut Map, + last_nonsigner_index: usize, + account_keys: &[Pubkey], + accounts: &[u8], + owner_field_name: &str, + multisig_field_name: &str, +) { + if accounts.len() > last_nonsigner_index + 1 { + let mut signers: Vec = vec![]; + for i in accounts[last_nonsigner_index + 1..].iter() { + signers.push(account_keys[*i as usize].to_string()); + } + map.insert( + multisig_field_name.to_string(), + json!(account_keys[accounts[last_nonsigner_index] as usize].to_string()), + ); + map.insert("signers".to_string(), json!(signers)); + } else { + map.insert( + owner_field_name.to_string(), + json!(account_keys[accounts[last_nonsigner_index] as usize].to_string()), + ); + } +} + +#[cfg(test)] +mod test { + use super::*; + use solana_sdk::instruction::CompiledInstruction; + use spl_token_v1_0::{ + instruction::*, + solana_sdk::{ + instruction::CompiledInstruction as SplTokenCompiledInstruction, message::Message, + pubkey::Pubkey as SplTokenPubkey, + }, + }; + use std::str::FromStr; + + fn convert_pubkey(pubkey: Pubkey) -> SplTokenPubkey { + SplTokenPubkey::from_str(&pubkey.to_string()).unwrap() + } + + fn convert_compiled_instruction( + instruction: &SplTokenCompiledInstruction, + ) -> CompiledInstruction { + CompiledInstruction { + program_id_index: instruction.program_id_index, + accounts: instruction.accounts.clone(), + data: instruction.data.clone(), + } + } + + #[test] + fn test_parse_token() { + let mut keys: Vec = vec![]; + for _ in 0..10 { + keys.push(Pubkey::new_rand()); + } + + // Test InitializeMint variations + let initialize_mint_ix = initialize_mint( + &spl_token_v1_0::id(), + &convert_pubkey(keys[0]), + Some(&convert_pubkey(keys[1])), + Some(&convert_pubkey(keys[2])), + 42, + 2, + ) + .unwrap(); + let message = Message::new(&[initialize_mint_ix], None); + let compiled_instruction = convert_compiled_instruction(&message.instructions[0]); + assert_eq!( + parse_token(&compiled_instruction, &keys).unwrap(), + json!({ + "type": "initializeMint", + "mint": keys[0].to_string(), + "amount": 42, + "decimals": 2, + "account": keys[1].to_string(), + "owner": keys[2].to_string(), + }) + ); + + let initialize_mint_ix = initialize_mint( + &spl_token_v1_0::id(), + &convert_pubkey(keys[0]), + Some(&convert_pubkey(keys[1])), + None, + 42, + 2, + ) + .unwrap(); + let message = Message::new(&[initialize_mint_ix], None); + let compiled_instruction = convert_compiled_instruction(&message.instructions[0]); + assert_eq!( + parse_token(&compiled_instruction, &keys).unwrap(), + json!({ + "type": "initializeMint", + "mint": keys[0].to_string(), + "amount": 42, + "decimals": 2, + "account": keys[1].to_string(), + }) + ); + + let initialize_mint_ix = initialize_mint( + &spl_token_v1_0::id(), + &convert_pubkey(keys[0]), + None, + Some(&convert_pubkey(keys[1])), + 0, + 2, + ) + .unwrap(); + let message = Message::new(&[initialize_mint_ix], None); + let compiled_instruction = convert_compiled_instruction(&message.instructions[0]); + assert_eq!( + parse_token(&compiled_instruction, &keys).unwrap(), + json!({ + "type": "initializeMint", + "mint": keys[0].to_string(), + "amount": 0, + "decimals": 2, + "owner": keys[1].to_string(), + }) + ); + + // Test InitializeAccount + let initialize_account_ix = initialize_account( + &spl_token_v1_0::id(), + &convert_pubkey(keys[0]), + &convert_pubkey(keys[1]), + &convert_pubkey(keys[2]), + ) + .unwrap(); + let message = Message::new(&[initialize_account_ix], None); + let compiled_instruction = convert_compiled_instruction(&message.instructions[0]); + assert_eq!( + parse_token(&compiled_instruction, &keys).unwrap(), + json!({ + "type": "initializeAccount", + "account": keys[0].to_string(), + "mint": keys[1].to_string(), + "owner": keys[2].to_string(), + }) + ); + + // Test InitializeMultisig + let initialize_multisig_ix = initialize_multisig( + &spl_token_v1_0::id(), + &convert_pubkey(keys[0]), + &[ + &convert_pubkey(keys[1]), + &convert_pubkey(keys[2]), + &convert_pubkey(keys[3]), + ], + 2, + ) + .unwrap(); + let message = Message::new(&[initialize_multisig_ix], None); + let compiled_instruction = convert_compiled_instruction(&message.instructions[0]); + assert_eq!( + parse_token(&compiled_instruction, &keys).unwrap(), + json!({ + "type": "initializeMultisig", + "multisig": keys[0].to_string(), + "m": 2, + "signers": keys[1..4].iter().map(|key| key.to_string()).collect::>(), + }) + ); + + // Test Transfer, incl multisig + let transfer_ix = transfer( + &spl_token_v1_0::id(), + &convert_pubkey(keys[1]), + &convert_pubkey(keys[2]), + &convert_pubkey(keys[0]), + &[], + 42, + ) + .unwrap(); + let message = Message::new(&[transfer_ix], None); + let compiled_instruction = convert_compiled_instruction(&message.instructions[0]); + assert_eq!( + parse_token(&compiled_instruction, &keys).unwrap(), + json!({ + "type": "transfer", + "source": keys[1].to_string(), + "destination": keys[2].to_string(), + "authority": keys[0].to_string(), + "amount": 42, + }) + ); + + let transfer_ix = transfer( + &spl_token_v1_0::id(), + &convert_pubkey(keys[2]), + &convert_pubkey(keys[3]), + &convert_pubkey(keys[4]), + &[&convert_pubkey(keys[0]), &convert_pubkey(keys[1])], + 42, + ) + .unwrap(); + let message = Message::new(&[transfer_ix], None); + let compiled_instruction = convert_compiled_instruction(&message.instructions[0]); + assert_eq!( + parse_token(&compiled_instruction, &keys).unwrap(), + json!({ + "type": "transfer", + "source": keys[2].to_string(), + "destination": keys[3].to_string(), + "multisigAuthority": keys[4].to_string(), + "signers": keys[0..2].iter().map(|key| key.to_string()).collect::>(), + "amount": 42, + }) + ); + + // Test Approve, incl multisig + let approve_ix = approve( + &spl_token_v1_0::id(), + &convert_pubkey(keys[1]), + &convert_pubkey(keys[2]), + &convert_pubkey(keys[0]), + &[], + 42, + ) + .unwrap(); + let message = Message::new(&[approve_ix], None); + let compiled_instruction = convert_compiled_instruction(&message.instructions[0]); + assert_eq!( + parse_token(&compiled_instruction, &keys).unwrap(), + json!({ + "type": "approve", + "source": keys[1].to_string(), + "delegate": keys[2].to_string(), + "owner": keys[0].to_string(), + "amount": 42, + }) + ); + + let approve_ix = approve( + &spl_token_v1_0::id(), + &convert_pubkey(keys[2]), + &convert_pubkey(keys[3]), + &convert_pubkey(keys[4]), + &[&convert_pubkey(keys[0]), &convert_pubkey(keys[1])], + 42, + ) + .unwrap(); + let message = Message::new(&[approve_ix], None); + let compiled_instruction = convert_compiled_instruction(&message.instructions[0]); + assert_eq!( + parse_token(&compiled_instruction, &keys).unwrap(), + json!({ + "type": "approve", + "source": keys[2].to_string(), + "delegate": keys[3].to_string(), + "multisigOwner": keys[4].to_string(), + "signers": keys[0..2].iter().map(|key| key.to_string()).collect::>(), + "amount": 42, + }) + ); + + // Test Revoke + let revoke_ix = revoke( + &spl_token_v1_0::id(), + &convert_pubkey(keys[1]), + &convert_pubkey(keys[0]), + &[], + ) + .unwrap(); + let message = Message::new(&[revoke_ix], None); + let compiled_instruction = convert_compiled_instruction(&message.instructions[0]); + assert_eq!( + parse_token(&compiled_instruction, &keys).unwrap(), + json!({ + "type": "revoke", + "source": keys[1].to_string(), + "owner": keys[0].to_string(), + }) + ); + + // Test SetOwner + let set_owner_ix = set_owner( + &spl_token_v1_0::id(), + &convert_pubkey(keys[1]), + &convert_pubkey(keys[2]), + &convert_pubkey(keys[0]), + &[], + ) + .unwrap(); + let message = Message::new(&[set_owner_ix], None); + let compiled_instruction = convert_compiled_instruction(&message.instructions[0]); + assert_eq!( + parse_token(&compiled_instruction, &keys).unwrap(), + json!({ + "type": "setOwner", + "owned": keys[1].to_string(), + "newOwner": keys[2].to_string(), + "owner": keys[0].to_string(), + }) + ); + + // Test MintTo + let mint_to_ix = mint_to( + &spl_token_v1_0::id(), + &convert_pubkey(keys[1]), + &convert_pubkey(keys[2]), + &convert_pubkey(keys[0]), + &[], + 42, + ) + .unwrap(); + let message = Message::new(&[mint_to_ix], None); + let compiled_instruction = convert_compiled_instruction(&message.instructions[0]); + assert_eq!( + parse_token(&compiled_instruction, &keys).unwrap(), + json!({ + "type": "mintTo", + "mint": keys[1].to_string(), + "account": keys[2].to_string(), + "owner": keys[0].to_string(), + "amount": 42, + }) + ); + + // Test Burn + let burn_ix = burn( + &spl_token_v1_0::id(), + &convert_pubkey(keys[1]), + &convert_pubkey(keys[0]), + &[], + 42, + ) + .unwrap(); + let message = Message::new(&[burn_ix], None); + let compiled_instruction = convert_compiled_instruction(&message.instructions[0]); + assert_eq!( + parse_token(&compiled_instruction, &keys).unwrap(), + json!({ + "type": "burn", + "account": keys[1].to_string(), + "authority": keys[0].to_string(), + "amount": 42, + }) + ); + + // Test CloseAccount + let close_account_ix = close_account( + &spl_token_v1_0::id(), + &convert_pubkey(keys[1]), + &convert_pubkey(keys[2]), + &convert_pubkey(keys[0]), + &[], + ) + .unwrap(); + let message = Message::new(&[close_account_ix], None); + let compiled_instruction = convert_compiled_instruction(&message.instructions[0]); + assert_eq!( + parse_token(&compiled_instruction, &keys).unwrap(), + json!({ + "type": "closeAccount", + "account": keys[1].to_string(), + "destination": keys[2].to_string(), + "owner": keys[0].to_string(), + }) + ); + } + + #[test] + fn test_token_ix_not_enough_keys() { + let mut keys: Vec = vec![]; + for _ in 0..10 { + keys.push(Pubkey::new_rand()); + } + + // Test InitializeMint variations + let initialize_mint_ix = initialize_mint( + &spl_token_v1_0::id(), + &convert_pubkey(keys[0]), + Some(&convert_pubkey(keys[1])), + Some(&convert_pubkey(keys[2])), + 42, + 2, + ) + .unwrap(); + let message = Message::new(&[initialize_mint_ix], None); + let mut compiled_instruction = convert_compiled_instruction(&message.instructions[0]); + assert!(parse_token(&compiled_instruction, &keys[0..2]).is_err()); + compiled_instruction.accounts = + compiled_instruction.accounts[0..compiled_instruction.accounts.len() - 2].to_vec(); + assert!(parse_token(&compiled_instruction, &keys).is_err()); + + let initialize_mint_ix = initialize_mint( + &spl_token_v1_0::id(), + &convert_pubkey(keys[0]), + Some(&convert_pubkey(keys[1])), + None, + 42, + 2, + ) + .unwrap(); + let message = Message::new(&[initialize_mint_ix], None); + let mut compiled_instruction = convert_compiled_instruction(&message.instructions[0]); + assert!(parse_token(&compiled_instruction, &keys[0..1]).is_err()); + compiled_instruction.accounts = + compiled_instruction.accounts[0..compiled_instruction.accounts.len() - 1].to_vec(); + assert!(parse_token(&compiled_instruction, &keys).is_err()); + + let initialize_mint_ix = initialize_mint( + &spl_token_v1_0::id(), + &convert_pubkey(keys[0]), + None, + Some(&convert_pubkey(keys[1])), + 0, + 2, + ) + .unwrap(); + let message = Message::new(&[initialize_mint_ix], None); + let mut compiled_instruction = convert_compiled_instruction(&message.instructions[0]); + assert!(parse_token(&compiled_instruction, &keys[0..1]).is_err()); + compiled_instruction.accounts = + compiled_instruction.accounts[0..compiled_instruction.accounts.len() - 1].to_vec(); + assert!(parse_token(&compiled_instruction, &keys).is_err()); + + // Test InitializeAccount + let initialize_account_ix = initialize_account( + &spl_token_v1_0::id(), + &convert_pubkey(keys[0]), + &convert_pubkey(keys[1]), + &convert_pubkey(keys[2]), + ) + .unwrap(); + let message = Message::new(&[initialize_account_ix], None); + let mut compiled_instruction = convert_compiled_instruction(&message.instructions[0]); + assert!(parse_token(&compiled_instruction, &keys[0..2]).is_err()); + compiled_instruction.accounts = + compiled_instruction.accounts[0..compiled_instruction.accounts.len() - 1].to_vec(); + assert!(parse_token(&compiled_instruction, &keys).is_err()); + + // Test InitializeMultisig + let initialize_multisig_ix = initialize_multisig( + &spl_token_v1_0::id(), + &convert_pubkey(keys[0]), + &[ + &convert_pubkey(keys[1]), + &convert_pubkey(keys[2]), + &convert_pubkey(keys[3]), + ], + 2, + ) + .unwrap(); + let message = Message::new(&[initialize_multisig_ix], None); + let mut compiled_instruction = convert_compiled_instruction(&message.instructions[0]); + assert!(parse_token(&compiled_instruction, &keys[0..3]).is_err()); + compiled_instruction.accounts = + compiled_instruction.accounts[0..compiled_instruction.accounts.len() - 3].to_vec(); + assert!(parse_token(&compiled_instruction, &keys).is_err()); + + // Test Transfer, incl multisig + let transfer_ix = transfer( + &spl_token_v1_0::id(), + &convert_pubkey(keys[1]), + &convert_pubkey(keys[2]), + &convert_pubkey(keys[0]), + &[], + 42, + ) + .unwrap(); + let message = Message::new(&[transfer_ix], None); + let mut compiled_instruction = convert_compiled_instruction(&message.instructions[0]); + assert!(parse_token(&compiled_instruction, &keys[0..2]).is_err()); + compiled_instruction.accounts = + compiled_instruction.accounts[0..compiled_instruction.accounts.len() - 1].to_vec(); + assert!(parse_token(&compiled_instruction, &keys).is_err()); + + let transfer_ix = transfer( + &spl_token_v1_0::id(), + &convert_pubkey(keys[2]), + &convert_pubkey(keys[3]), + &convert_pubkey(keys[4]), + &[&convert_pubkey(keys[0]), &convert_pubkey(keys[1])], + 42, + ) + .unwrap(); + let message = Message::new(&[transfer_ix], None); + let mut compiled_instruction = convert_compiled_instruction(&message.instructions[0]); + assert!(parse_token(&compiled_instruction, &keys[0..4]).is_err()); + compiled_instruction.accounts = + compiled_instruction.accounts[0..compiled_instruction.accounts.len() - 3].to_vec(); + assert!(parse_token(&compiled_instruction, &keys).is_err()); + + // Test Approve, incl multisig + let approve_ix = approve( + &spl_token_v1_0::id(), + &convert_pubkey(keys[1]), + &convert_pubkey(keys[2]), + &convert_pubkey(keys[0]), + &[], + 42, + ) + .unwrap(); + let message = Message::new(&[approve_ix], None); + let mut compiled_instruction = convert_compiled_instruction(&message.instructions[0]); + assert!(parse_token(&compiled_instruction, &keys[0..2]).is_err()); + compiled_instruction.accounts = + compiled_instruction.accounts[0..compiled_instruction.accounts.len() - 1].to_vec(); + assert!(parse_token(&compiled_instruction, &keys).is_err()); + + let approve_ix = approve( + &spl_token_v1_0::id(), + &convert_pubkey(keys[2]), + &convert_pubkey(keys[3]), + &convert_pubkey(keys[4]), + &[&convert_pubkey(keys[0]), &convert_pubkey(keys[1])], + 42, + ) + .unwrap(); + let message = Message::new(&[approve_ix], None); + let mut compiled_instruction = convert_compiled_instruction(&message.instructions[0]); + assert!(parse_token(&compiled_instruction, &keys[0..4]).is_err()); + compiled_instruction.accounts = + compiled_instruction.accounts[0..compiled_instruction.accounts.len() - 3].to_vec(); + assert!(parse_token(&compiled_instruction, &keys).is_err()); + + // Test Revoke + let revoke_ix = revoke( + &spl_token_v1_0::id(), + &convert_pubkey(keys[1]), + &convert_pubkey(keys[0]), + &[], + ) + .unwrap(); + let message = Message::new(&[revoke_ix], None); + let mut compiled_instruction = convert_compiled_instruction(&message.instructions[0]); + assert!(parse_token(&compiled_instruction, &keys[0..1]).is_err()); + compiled_instruction.accounts = + compiled_instruction.accounts[0..compiled_instruction.accounts.len() - 1].to_vec(); + assert!(parse_token(&compiled_instruction, &keys).is_err()); + + // Test SetOwner + let set_owner_ix = set_owner( + &spl_token_v1_0::id(), + &convert_pubkey(keys[1]), + &convert_pubkey(keys[2]), + &convert_pubkey(keys[0]), + &[], + ) + .unwrap(); + let message = Message::new(&[set_owner_ix], None); + let mut compiled_instruction = convert_compiled_instruction(&message.instructions[0]); + assert!(parse_token(&compiled_instruction, &keys[0..2]).is_err()); + compiled_instruction.accounts = + compiled_instruction.accounts[0..compiled_instruction.accounts.len() - 1].to_vec(); + assert!(parse_token(&compiled_instruction, &keys).is_err()); + + // Test MintTo + let mint_to_ix = mint_to( + &spl_token_v1_0::id(), + &convert_pubkey(keys[1]), + &convert_pubkey(keys[2]), + &convert_pubkey(keys[0]), + &[], + 42, + ) + .unwrap(); + let message = Message::new(&[mint_to_ix], None); + let mut compiled_instruction = convert_compiled_instruction(&message.instructions[0]); + assert!(parse_token(&compiled_instruction, &keys[0..2]).is_err()); + compiled_instruction.accounts = + compiled_instruction.accounts[0..compiled_instruction.accounts.len() - 1].to_vec(); + assert!(parse_token(&compiled_instruction, &keys).is_err()); + + // Test Burn + let burn_ix = burn( + &spl_token_v1_0::id(), + &convert_pubkey(keys[1]), + &convert_pubkey(keys[0]), + &[], + 42, + ) + .unwrap(); + let message = Message::new(&[burn_ix], None); + let mut compiled_instruction = convert_compiled_instruction(&message.instructions[0]); + assert!(parse_token(&compiled_instruction, &keys[0..1]).is_err()); + compiled_instruction.accounts = + compiled_instruction.accounts[0..compiled_instruction.accounts.len() - 1].to_vec(); + assert!(parse_token(&compiled_instruction, &keys).is_err()); + + // Test CloseAccount + let close_account_ix = close_account( + &spl_token_v1_0::id(), + &convert_pubkey(keys[1]), + &convert_pubkey(keys[2]), + &convert_pubkey(keys[0]), + &[], + ) + .unwrap(); + let message = Message::new(&[close_account_ix], None); + let mut compiled_instruction = convert_compiled_instruction(&message.instructions[0]); + assert!(parse_token(&compiled_instruction, &keys[0..2]).is_err()); + compiled_instruction.accounts = + compiled_instruction.accounts[0..compiled_instruction.accounts.len() - 1].to_vec(); + assert!(parse_token(&compiled_instruction, &keys).is_err()); + } +}