diff --git a/Cargo.lock b/Cargo.lock index 504bfcfad..304eeb244 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6559,6 +6559,7 @@ dependencies = [ "serde_derive", "serde_json", "solana-account-decoder", + "solana-address-lookup-table-program", "solana-measure", "solana-metrics", "solana-sdk 1.12.0", diff --git a/docs/src/developing/clients/jsonrpc-api.md b/docs/src/developing/clients/jsonrpc-api.md index 7f91201f2..4d19df8a4 100644 --- a/docs/src/developing/clients/jsonrpc-api.md +++ b/docs/src/developing/clients/jsonrpc-api.md @@ -214,7 +214,7 @@ JSON parsing for the following native and SPL programs: | Program | Account State | Instructions | | --- | --- | --- | -| Address Lookup | v1.12.0 | | +| Address Lookup | v1.12.0 | v1.12.0 | | BPF Loader | n/a | stable | | BPF Upgradeable Loader | stable | stable | | Config | stable | | diff --git a/programs/bpf/Cargo.lock b/programs/bpf/Cargo.lock index f1e6ae70f..cd6acd53e 100644 --- a/programs/bpf/Cargo.lock +++ b/programs/bpf/Cargo.lock @@ -5819,6 +5819,7 @@ dependencies = [ "serde_derive", "serde_json", "solana-account-decoder", + "solana-address-lookup-table-program", "solana-measure", "solana-metrics", "solana-sdk 1.12.0", diff --git a/transaction-status/Cargo.toml b/transaction-status/Cargo.toml index 9d59696e0..33ca98863 100644 --- a/transaction-status/Cargo.toml +++ b/transaction-status/Cargo.toml @@ -21,6 +21,7 @@ serde = "1.0.143" serde_derive = "1.0.103" serde_json = "1.0.83" solana-account-decoder = { path = "../account-decoder", version = "=1.12.0" } +solana-address-lookup-table-program = { path = "../programs/address-lookup-table", version = "=1.12.0" } solana-measure = { path = "../measure", version = "=1.12.0" } solana-metrics = { path = "../metrics", version = "=1.12.0" } solana-sdk = { path = "../sdk", version = "=1.12.0" } diff --git a/transaction-status/src/lib.rs b/transaction-status/src/lib.rs index 9bf5eb9ff..9bfe29fce 100644 --- a/transaction-status/src/lib.rs +++ b/transaction-status/src/lib.rs @@ -34,6 +34,7 @@ extern crate serde_derive; pub mod extract_memos; pub mod parse_accounts; +pub mod parse_address_lookup_table; pub mod parse_associated_token; pub mod parse_bpf_loader; pub mod parse_instruction; diff --git a/transaction-status/src/parse_address_lookup_table.rs b/transaction-status/src/parse_address_lookup_table.rs new file mode 100644 index 000000000..f30b61ad7 --- /dev/null +++ b/transaction-status/src/parse_address_lookup_table.rs @@ -0,0 +1,358 @@ +use { + crate::parse_instruction::{ + check_num_accounts, ParsableProgram, ParseInstructionError, ParsedInstructionEnum, + }, + bincode::deserialize, + serde_json::json, + solana_address_lookup_table_program::instruction::ProgramInstruction, + solana_sdk::{instruction::CompiledInstruction, message::AccountKeys}, +}; + +pub fn parse_address_lookup_table( + instruction: &CompiledInstruction, + account_keys: &AccountKeys, +) -> Result { + let address_lookup_table_instruction: ProgramInstruction = deserialize(&instruction.data) + .map_err(|_| { + ParseInstructionError::InstructionNotParsable(ParsableProgram::AddressLookupTable) + })?; + 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::AddressLookupTable, + )); + } + } + match address_lookup_table_instruction { + ProgramInstruction::CreateLookupTable { + recent_slot, + bump_seed, + } => { + check_num_address_lookup_table_accounts(&instruction.accounts, 4)?; + Ok(ParsedInstructionEnum { + instruction_type: "createLookupTable".to_string(), + info: json!({ + "lookupTableAccount": account_keys[instruction.accounts[0] as usize].to_string(), + "lookupTableAuthority": account_keys[instruction.accounts[1] as usize].to_string(), + "payerAccount": account_keys[instruction.accounts[2] as usize].to_string(), + "systemProgram": account_keys[instruction.accounts[3] as usize].to_string(), + "recentSlot": recent_slot, + "bumpSeed": bump_seed, + }), + }) + } + ProgramInstruction::FreezeLookupTable => { + check_num_address_lookup_table_accounts(&instruction.accounts, 2)?; + Ok(ParsedInstructionEnum { + instruction_type: "freezeLookupTable".to_string(), + info: json!({ + "lookupTableAccount": account_keys[instruction.accounts[0] as usize].to_string(), + "lookupTableAuthority": account_keys[instruction.accounts[1] as usize].to_string(), + }), + }) + } + ProgramInstruction::ExtendLookupTable { new_addresses } => { + check_num_address_lookup_table_accounts(&instruction.accounts, 2)?; + let new_addresses: Vec = new_addresses + .into_iter() + .map(|address| address.to_string()) + .collect(); + let mut value = json!({ + "lookupTableAccount": account_keys[instruction.accounts[0] as usize].to_string(), + "lookupTableAuthority": account_keys[instruction.accounts[1] as usize].to_string(), + "newAddresses": new_addresses, + }); + let map = value.as_object_mut().unwrap(); + if instruction.accounts.len() >= 4 { + map.insert( + "payerAccount".to_string(), + json!(account_keys[instruction.accounts[2] as usize].to_string()), + ); + map.insert( + "systemProgram".to_string(), + json!(account_keys[instruction.accounts[3] as usize].to_string()), + ); + } + Ok(ParsedInstructionEnum { + instruction_type: "extendLookupTable".to_string(), + info: value, + }) + } + ProgramInstruction::DeactivateLookupTable => { + check_num_address_lookup_table_accounts(&instruction.accounts, 2)?; + Ok(ParsedInstructionEnum { + instruction_type: "deactivateLookupTable".to_string(), + info: json!({ + "lookupTableAccount": account_keys[instruction.accounts[0] as usize].to_string(), + "lookupTableAuthority": account_keys[instruction.accounts[1] as usize].to_string(), + }), + }) + } + ProgramInstruction::CloseLookupTable => { + check_num_address_lookup_table_accounts(&instruction.accounts, 3)?; + Ok(ParsedInstructionEnum { + instruction_type: "closeLookupTable".to_string(), + info: json!({ + "lookupTableAccount": account_keys[instruction.accounts[0] as usize].to_string(), + "lookupTableAuthority": account_keys[instruction.accounts[1] as usize].to_string(), + "recipient": account_keys[instruction.accounts[2] as usize].to_string(), + }), + }) + } + } +} + +fn check_num_address_lookup_table_accounts( + accounts: &[u8], + num: usize, +) -> Result<(), ParseInstructionError> { + check_num_accounts(accounts, num, ParsableProgram::AddressLookupTable) +} + +#[cfg(test)] +mod test { + use { + super::*, + solana_address_lookup_table_program::instruction, + solana_sdk::{message::Message, pubkey::Pubkey, system_program}, + std::str::FromStr, + }; + + #[test] + fn test_parse_create_address_lookup_table_ix() { + let from_pubkey = Pubkey::new_unique(); + // use explicit key to have predicatble bump_seed + let authority = Pubkey::from_str("HkxY6vXdrKzoCQLmdJ3cYo9534FdZQxzBNWTyrJzzqJM").unwrap(); + let slot = 42; + + let (instruction, lookup_table_pubkey) = + instruction::create_lookup_table(authority, from_pubkey, slot); + let mut message = Message::new(&[instruction], None); + assert_eq!( + parse_address_lookup_table( + &message.instructions[0], + &AccountKeys::new(&message.account_keys, None) + ) + .unwrap(), + ParsedInstructionEnum { + instruction_type: "createLookupTable".to_string(), + info: json!({ + "lookupTableAccount": lookup_table_pubkey.to_string(), + "lookupTableAuthority": authority.to_string(), + "payerAccount": from_pubkey.to_string(), + "systemProgram": system_program::id().to_string(), + "recentSlot": slot, + "bumpSeed": 254, + }), + } + ); + assert!(parse_address_lookup_table( + &message.instructions[0], + &AccountKeys::new(&message.account_keys[0..3], None) + ) + .is_err()); + let keys = message.account_keys.clone(); + message.instructions[0].accounts.pop(); + assert!(parse_address_lookup_table( + &message.instructions[0], + &AccountKeys::new(&keys, None) + ) + .is_err()); + } + + #[test] + fn test_parse_freeze_lookup_table_ix() { + let lookup_table_pubkey = Pubkey::new_unique(); + let authority = Pubkey::new_unique(); + + let instruction = instruction::freeze_lookup_table(lookup_table_pubkey, authority); + let mut message = Message::new(&[instruction], None); + assert_eq!( + parse_address_lookup_table( + &message.instructions[0], + &AccountKeys::new(&message.account_keys, None) + ) + .unwrap(), + ParsedInstructionEnum { + instruction_type: "freezeLookupTable".to_string(), + info: json!({ + "lookupTableAccount": lookup_table_pubkey.to_string(), + "lookupTableAuthority": authority.to_string(), + }), + } + ); + assert!(parse_address_lookup_table( + &message.instructions[0], + &AccountKeys::new(&message.account_keys[0..1], None) + ) + .is_err()); + let keys = message.account_keys.clone(); + message.instructions[0].accounts.pop(); + assert!(parse_address_lookup_table( + &message.instructions[0], + &AccountKeys::new(&keys, None) + ) + .is_err()); + } + + #[test] + fn test_parse_extend_lookup_table_ix() { + let lookup_table_pubkey = Pubkey::new_unique(); + let authority = Pubkey::new_unique(); + let from_pubkey = Pubkey::new_unique(); + let no_addresses = vec![]; + let address0 = Pubkey::new_unique(); + let address1 = Pubkey::new_unique(); + let some_addresses = vec![address0, address1]; + + // No payer, no addresses + let instruction = + instruction::extend_lookup_table(lookup_table_pubkey, authority, None, no_addresses); + let mut message = Message::new(&[instruction], None); + assert_eq!( + parse_address_lookup_table( + &message.instructions[0], + &AccountKeys::new(&message.account_keys, None) + ) + .unwrap(), + ParsedInstructionEnum { + instruction_type: "extendLookupTable".to_string(), + info: json!({ + "lookupTableAccount": lookup_table_pubkey.to_string(), + "lookupTableAuthority": authority.to_string(), + "newAddresses": [], + }), + } + ); + assert!(parse_address_lookup_table( + &message.instructions[0], + &AccountKeys::new(&message.account_keys[0..1], None) + ) + .is_err()); + let keys = message.account_keys.clone(); + message.instructions[0].accounts.pop(); + assert!(parse_address_lookup_table( + &message.instructions[0], + &AccountKeys::new(&keys, None) + ) + .is_err()); + + // Some payer, some addresses + let instruction = instruction::extend_lookup_table( + lookup_table_pubkey, + authority, + Some(from_pubkey), + some_addresses, + ); + let mut message = Message::new(&[instruction], None); + assert_eq!( + parse_address_lookup_table( + &message.instructions[0], + &AccountKeys::new(&message.account_keys, None) + ) + .unwrap(), + ParsedInstructionEnum { + instruction_type: "extendLookupTable".to_string(), + info: json!({ + "lookupTableAccount": lookup_table_pubkey.to_string(), + "lookupTableAuthority": authority.to_string(), + "payerAccount": from_pubkey.to_string(), + "systemProgram": system_program::id().to_string(), + "newAddresses": [ + address0.to_string(), + address1.to_string(), + ], + }), + } + ); + assert!(parse_address_lookup_table( + &message.instructions[0], + &AccountKeys::new(&message.account_keys[0..1], None) + ) + .is_err()); + let keys = message.account_keys.clone(); + message.instructions[0].accounts.pop(); + message.instructions[0].accounts.pop(); + message.instructions[0].accounts.pop(); + assert!(parse_address_lookup_table( + &message.instructions[0], + &AccountKeys::new(&keys, None) + ) + .is_err()); + } + + #[test] + fn test_parse_deactivate_lookup_table_ix() { + let lookup_table_pubkey = Pubkey::new_unique(); + let authority = Pubkey::new_unique(); + + let instruction = instruction::deactivate_lookup_table(lookup_table_pubkey, authority); + let mut message = Message::new(&[instruction], None); + assert_eq!( + parse_address_lookup_table( + &message.instructions[0], + &AccountKeys::new(&message.account_keys, None) + ) + .unwrap(), + ParsedInstructionEnum { + instruction_type: "deactivateLookupTable".to_string(), + info: json!({ + "lookupTableAccount": lookup_table_pubkey.to_string(), + "lookupTableAuthority": authority.to_string(), + }), + } + ); + assert!(parse_address_lookup_table( + &message.instructions[0], + &AccountKeys::new(&message.account_keys[0..1], None) + ) + .is_err()); + let keys = message.account_keys.clone(); + message.instructions[0].accounts.pop(); + assert!(parse_address_lookup_table( + &message.instructions[0], + &AccountKeys::new(&keys, None) + ) + .is_err()); + } + + #[test] + fn test_parse_close_lookup_table_ix() { + let lookup_table_pubkey = Pubkey::new_unique(); + let authority = Pubkey::new_unique(); + let recipient = Pubkey::new_unique(); + + let instruction = + instruction::close_lookup_table(lookup_table_pubkey, authority, recipient); + let mut message = Message::new(&[instruction], None); + assert_eq!( + parse_address_lookup_table( + &message.instructions[0], + &AccountKeys::new(&message.account_keys, None) + ) + .unwrap(), + ParsedInstructionEnum { + instruction_type: "closeLookupTable".to_string(), + info: json!({ + "lookupTableAccount": lookup_table_pubkey.to_string(), + "lookupTableAuthority": authority.to_string(), + "recipient": recipient.to_string(), + }), + } + ); + assert!(parse_address_lookup_table( + &message.instructions[0], + &AccountKeys::new(&message.account_keys[0..2], None) + ) + .is_err()); + let keys = message.account_keys.clone(); + message.instructions[0].accounts.pop(); + assert!(parse_address_lookup_table( + &message.instructions[0], + &AccountKeys::new(&keys, None) + ) + .is_err()); + } +} diff --git a/transaction-status/src/parse_instruction.rs b/transaction-status/src/parse_instruction.rs index 29f812c41..4689e9c10 100644 --- a/transaction-status/src/parse_instruction.rs +++ b/transaction-status/src/parse_instruction.rs @@ -1,6 +1,7 @@ use { crate::{ extract_memos::{spl_memo_id_v1, spl_memo_id_v3}, + parse_address_lookup_table::parse_address_lookup_table, parse_associated_token::{parse_associated_token, spl_associated_token_id}, parse_bpf_loader::{parse_bpf_loader, parse_bpf_upgradeable_loader}, parse_stake::parse_stake, @@ -23,6 +24,7 @@ use { }; lazy_static! { + static ref ADDRESS_LOOKUP_PROGRAM_ID: Pubkey = solana_address_lookup_table_program::id(); static ref ASSOCIATED_TOKEN_PROGRAM_ID: Pubkey = spl_associated_token_id(); 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(); @@ -33,6 +35,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( + *ADDRESS_LOOKUP_PROGRAM_ID, + ParsableProgram::AddressLookupTable, + ); m.insert( *ASSOCIATED_TOKEN_PROGRAM_ID, ParsableProgram::SplAssociatedTokenAccount, @@ -89,6 +95,7 @@ pub struct ParsedInstructionEnum { #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub enum ParsableProgram { + AddressLookupTable, SplAssociatedTokenAccount, SplMemo, SplToken, @@ -108,6 +115,9 @@ pub fn parse( .get(program_id) .ok_or(ParseInstructionError::ProgramNotParsable)?; let parsed_json = match program_name { + ParsableProgram::AddressLookupTable => { + serde_json::to_value(parse_address_lookup_table(instruction, account_keys)?)? + } ParsableProgram::SplAssociatedTokenAccount => { serde_json::to_value(parse_associated_token(instruction, account_keys)?)? }