diff --git a/Cargo.lock b/Cargo.lock index 6954008e3b..c921425bed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5377,6 +5377,7 @@ dependencies = [ "solana-sdk", "solana-stake-program", "solana-vote-program", + "spl-associated-token-account", "spl-memo 2.0.1", "spl-memo 3.0.0", "spl-token", diff --git a/programs/bpf/Cargo.lock b/programs/bpf/Cargo.lock index 7a3dfd248a..de60744190 100644 --- a/programs/bpf/Cargo.lock +++ b/programs/bpf/Cargo.lock @@ -3359,6 +3359,7 @@ dependencies = [ "solana-sdk", "solana-stake-program", "solana-vote-program", + "spl-associated-token-account", "spl-memo 2.0.1", "spl-memo 3.0.0", "spl-token", @@ -3422,6 +3423,16 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" +[[package]] +name = "spl-associated-token-account" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4adc47eebe5d2b662cbaaba1843719c28a67e5ec5d0460bc3ca60900a51f74e2" +dependencies = [ + "solana-program 1.5.8", + "spl-token", +] + [[package]] name = "spl-memo" version = "2.0.1" diff --git a/transaction-status/Cargo.toml b/transaction-status/Cargo.toml index 4193db7007..267999ab9d 100644 --- a/transaction-status/Cargo.toml +++ b/transaction-status/Cargo.toml @@ -23,6 +23,7 @@ solana-sdk = { path = "../sdk", version = "=1.7.0" } solana-runtime = { path = "../runtime", version = "=1.7.0" } solana-stake-program = { path = "../programs/stake", version = "=1.7.0" } solana-vote-program = { path = "../programs/vote", version = "=1.7.0" } +spl-associated-token-account-v1-0 = { package = "spl-associated-token-account", version = "=1.0.2", features = ["no-entrypoint"] } spl-memo-v1-0 = { package = "spl-memo", version = "=2.0.1", features = ["no-entrypoint"] } spl-memo-v3-0 = { package = "spl-memo", version = "=3.0.0", features = ["no-entrypoint"] } spl-token-v2-0 = { package = "spl-token", version = "=3.1.0", features = ["no-entrypoint"] } diff --git a/transaction-status/src/lib.rs b/transaction-status/src/lib.rs index e855e45952..942e581326 100644 --- a/transaction-status/src/lib.rs +++ b/transaction-status/src/lib.rs @@ -5,6 +5,7 @@ extern crate lazy_static; extern crate serde_derive; pub mod parse_accounts; +pub mod parse_associated_token; pub mod parse_bpf_loader; pub mod parse_instruction; pub mod parse_stake; diff --git a/transaction-status/src/parse_associated_token.rs b/transaction-status/src/parse_associated_token.rs new file mode 100644 index 0000000000..a50268a00d --- /dev/null +++ b/transaction-status/src/parse_associated_token.rs @@ -0,0 +1,104 @@ +use crate::parse_instruction::{ + check_num_accounts, ParsableProgram, ParseInstructionError, ParsedInstructionEnum, +}; +use serde_json::json; +use solana_sdk::{instruction::CompiledInstruction, pubkey::Pubkey}; +use std::str::FromStr; + +// A helper function to convert spl_associated_token_account_v1_0::id() as spl_sdk::pubkey::Pubkey +// to solana_sdk::pubkey::Pubkey +pub fn spl_associated_token_id_v1_0() -> Pubkey { + Pubkey::from_str(&spl_associated_token_account_v1_0::id().to_string()).unwrap() +} + +pub fn parse_associated_token( + instruction: &CompiledInstruction, + account_keys: &[Pubkey], +) -> Result { + 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::SplAssociatedTokenAccount, + )); + } + } + check_num_associated_token_accounts(&instruction.accounts, 7)?; + Ok(ParsedInstructionEnum { + instruction_type: "create".to_string(), + info: json!({ + "source": account_keys[instruction.accounts[0] as usize].to_string(), + "account": account_keys[instruction.accounts[1] as usize].to_string(), + "wallet": account_keys[instruction.accounts[2] as usize].to_string(), + "mint": account_keys[instruction.accounts[3] as usize].to_string(), + "systemProgram": account_keys[instruction.accounts[4] as usize].to_string(), + "tokenProgram": account_keys[instruction.accounts[5] as usize].to_string(), + "rentSysvar": account_keys[instruction.accounts[6] as usize].to_string(), + }), + }) +} + +fn check_num_associated_token_accounts( + accounts: &[u8], + num: usize, +) -> Result<(), ParseInstructionError> { + check_num_accounts(accounts, num, ParsableProgram::SplAssociatedTokenAccount) +} + +#[cfg(test)] +mod test { + use super::*; + use spl_associated_token_account_v1_0::{ + create_associated_token_account, + solana_program::{ + instruction::CompiledInstruction as SplAssociatedTokenCompiledInstruction, + message::Message, pubkey::Pubkey as SplAssociatedTokenPubkey, + }, + }; + + fn convert_pubkey(pubkey: Pubkey) -> SplAssociatedTokenPubkey { + SplAssociatedTokenPubkey::from_str(&pubkey.to_string()).unwrap() + } + + fn convert_compiled_instruction( + instruction: &SplAssociatedTokenCompiledInstruction, + ) -> CompiledInstruction { + CompiledInstruction { + program_id_index: instruction.program_id_index, + accounts: instruction.accounts.clone(), + data: instruction.data.clone(), + } + } + + #[test] + fn test_parse_associated_token() { + let mut keys: Vec = vec![]; + for _ in 0..7 { + keys.push(solana_sdk::pubkey::new_rand()); + } + + let create_ix = create_associated_token_account( + &convert_pubkey(keys[0]), + &convert_pubkey(keys[1]), + &convert_pubkey(keys[2]), + ); + let message = Message::new(&[create_ix], None); + let compiled_instruction = convert_compiled_instruction(&message.instructions[0]); + assert_eq!( + parse_associated_token(&compiled_instruction, &keys).unwrap(), + ParsedInstructionEnum { + instruction_type: "create".to_string(), + info: json!({ + "source": keys[0].to_string(), + "account": keys[1].to_string(), + "wallet": keys[2].to_string(), + "mint": keys[3].to_string(), + "systemProgram": keys[4].to_string(), + "tokenProgram": keys[5].to_string(), + "rentSysvar": keys[6].to_string(), + }) + } + ); + } +} diff --git a/transaction-status/src/parse_instruction.rs b/transaction-status/src/parse_instruction.rs index ab9d3326cc..dea1c2133e 100644 --- a/transaction-status/src/parse_instruction.rs +++ b/transaction-status/src/parse_instruction.rs @@ -1,4 +1,5 @@ use crate::{ + parse_associated_token::{parse_associated_token, spl_associated_token_id_v1_0}, parse_bpf_loader::{parse_bpf_loader, parse_bpf_upgradeable_loader}, parse_stake::parse_stake, parse_system::parse_system, @@ -16,6 +17,7 @@ use std::{ use thiserror::Error; lazy_static! { + static ref ASSOCIATED_TOKEN_PROGRAM_ID: Pubkey = spl_associated_token_id_v1_0(); static ref BPF_LOADER_PROGRAM_ID: Pubkey = solana_sdk::bpf_loader::id(); static ref BPF_UPGRADEABLE_LOADER_PROGRAM_ID: Pubkey = solana_sdk::bpf_loader_upgradeable::id(); static ref MEMO_V1_PROGRAM_ID: Pubkey = @@ -28,6 +30,10 @@ lazy_static! { static ref VOTE_PROGRAM_ID: Pubkey = solana_vote_program::id(); static ref PARSABLE_PROGRAM_IDS: HashMap = { let mut m = HashMap::new(); + m.insert( + *ASSOCIATED_TOKEN_PROGRAM_ID, + ParsableProgram::SplAssociatedTokenAccount, + ); m.insert(*MEMO_V1_PROGRAM_ID, ParsableProgram::SplMemo); m.insert(*MEMO_V3_PROGRAM_ID, ParsableProgram::SplMemo); m.insert(*TOKEN_PROGRAM_ID, ParsableProgram::SplToken); @@ -78,6 +84,7 @@ pub struct ParsedInstructionEnum { #[derive(Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub enum ParsableProgram { + SplAssociatedTokenAccount, SplMemo, SplToken, BpfLoader, @@ -96,6 +103,9 @@ pub fn parse( .get(program_id) .ok_or(ParseInstructionError::ProgramNotParsable)?; let parsed_json = match program_name { + ParsableProgram::SplAssociatedTokenAccount => { + serde_json::to_value(parse_associated_token(instruction, account_keys)?)? + } ParsableProgram::SplMemo => parse_memo(instruction), ParsableProgram::SplToken => serde_json::to_value(parse_token(instruction, account_keys)?)?, ParsableProgram::BpfLoader => {