From c6edfc3944d95f1eb65a1fabda5dfefe46a1da10 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 5 Aug 2020 08:21:02 +0000 Subject: [PATCH] Update instruction encoding format (#11363) (#11379) * Rework parsed instruction format * Rework parsed message accounts * Review comments (cherry picked from commit 9d4f9be1fec26928f93fc6dbc02d3e92f206fa57) Co-authored-by: Tyera Eulberg --- transaction-status/src/lib.rs | 27 +- transaction-status/src/parse_accounts.rs | 64 +++-- transaction-status/src/parse_instruction.rs | 50 +++- transaction-status/src/parse_token.rs | 296 ++++++++++++-------- 4 files changed, 264 insertions(+), 173 deletions(-) diff --git a/transaction-status/src/lib.rs b/transaction-status/src/lib.rs index 8da45292ba..d470a47575 100644 --- a/transaction-status/src/lib.rs +++ b/transaction-status/src/lib.rs @@ -7,8 +7,10 @@ pub mod parse_accounts; pub mod parse_instruction; pub mod parse_token; -use crate::{parse_accounts::parse_accounts, parse_instruction::parse}; -use serde_json::{json, Value}; +use crate::{ + parse_accounts::{parse_accounts, ParsedAccount}, + parse_instruction::{parse, ParsedInstruction}, +}; use solana_sdk::{ clock::{Slot, UnixTimestamp}, commitment_config::CommitmentConfig, @@ -23,7 +25,14 @@ use solana_sdk::{ #[serde(rename_all = "camelCase", untagged)] pub enum UiInstruction { Compiled(UiCompiledInstruction), - Parsed(Value), + Parsed(UiParsedInstruction), +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", untagged)] +pub enum UiParsedInstruction { + Parsed(ParsedInstruction), + PartiallyDecoded(UiPartiallyDecodedInstruction), } /// A duplicate representation of a CompiledInstruction for pretty JSON serialization @@ -183,7 +192,7 @@ pub struct UiRawMessage { #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct UiParsedMessage { - pub account_keys: Value, + pub account_keys: Vec, pub recent_blockhash: String, pub instructions: Vec, } @@ -250,13 +259,15 @@ impl EncodedTransaction { instruction, &transaction.message.account_keys, ) { - UiInstruction::Parsed(parsed_instruction) + UiInstruction::Parsed(UiParsedInstruction::Parsed( + parsed_instruction, + )) } else { - UiInstruction::Parsed(json!( + UiInstruction::Parsed(UiParsedInstruction::PartiallyDecoded( UiPartiallyDecodedInstruction::from( instruction, - &transaction.message.account_keys - ) + &transaction.message.account_keys, + ), )) } }) diff --git a/transaction-status/src/parse_accounts.rs b/transaction-status/src/parse_accounts.rs index 6f3fc59d42..c8c478f3f9 100644 --- a/transaction-status/src/parse_accounts.rs +++ b/transaction-status/src/parse_accounts.rs @@ -1,28 +1,23 @@ -use serde_json::{json, Map, Value}; use solana_sdk::message::Message; -type AccountAttributes = Vec; - -#[derive(Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] -enum AccountAttribute { - Signer, - Writable, +pub struct ParsedAccount { + pub pubkey: String, + pub writable: bool, + pub signer: bool, } -pub fn parse_accounts(message: &Message) -> Value { - let mut accounts: Map = Map::new(); +pub fn parse_accounts(message: &Message) -> Vec { + let mut accounts: Vec = vec![]; for (i, account_key) in message.account_keys.iter().enumerate() { - let mut attributes: AccountAttributes = vec![]; - if message.is_writable(i) { - attributes.push(AccountAttribute::Writable); - } - if message.is_signer(i) { - attributes.push(AccountAttribute::Signer); - } - accounts.insert(account_key.to_string(), json!(attributes)); + accounts.push(ParsedAccount { + pubkey: account_key.to_string(), + writable: message.is_writable(i), + signer: message.is_signer(i), + }); } - json!(accounts) + accounts } #[cfg(test)] @@ -44,13 +39,30 @@ mod test { }; message.account_keys = vec![pubkey0, pubkey1, pubkey2, pubkey3]; - let expected_json = json!({ - pubkey0.to_string(): ["writable", "signer"], - pubkey1.to_string(): ["signer"], - pubkey2.to_string(): ["writable"], - pubkey3.to_string(): [], - }); - - assert_eq!(parse_accounts(&message), expected_json); + assert_eq!( + parse_accounts(&message), + vec![ + ParsedAccount { + pubkey: pubkey0.to_string(), + writable: true, + signer: true, + }, + ParsedAccount { + pubkey: pubkey1.to_string(), + writable: false, + signer: true, + }, + ParsedAccount { + pubkey: pubkey2.to_string(), + writable: true, + signer: false, + }, + ParsedAccount { + pubkey: pubkey3.to_string(), + writable: false, + signer: false, + }, + ] + ); } } diff --git a/transaction-status/src/parse_instruction.rs b/transaction-status/src/parse_instruction.rs index a08d92afae..aaa6ac0616 100644 --- a/transaction-status/src/parse_instruction.rs +++ b/transaction-status/src/parse_instruction.rs @@ -1,6 +1,6 @@ use crate::parse_token::parse_token; use inflector::Inflector; -use serde_json::{json, Value}; +use serde_json::Value; use solana_account_decoder::parse_token::spl_token_id_v1_0; use solana_sdk::{instruction::CompiledInstruction, pubkey::Pubkey}; use std::{ @@ -21,7 +21,7 @@ lazy_static! { }; } -#[derive(Error, Debug, PartialEq)] +#[derive(Error, Debug)] pub enum ParseInstructionError { #[error("{0:?} instruction not parsable")] InstructionNotParsable(ParsableProgram), @@ -31,6 +31,25 @@ pub enum ParseInstructionError { #[error("Program not parsable")] ProgramNotParsable, + + #[error("Internal error, please report")] + SerdeJsonError(#[from] serde_json::error::Error), +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct ParsedInstruction { + pub program: String, + pub program_id: String, + pub parsed: Value, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct ParsedInstructionEnum { + #[serde(rename = "type")] + pub instruction_type: String, + pub info: Value, } #[derive(Debug, Serialize, Deserialize, PartialEq)] @@ -44,17 +63,19 @@ pub fn parse( program_id: &Pubkey, instruction: &CompiledInstruction, account_keys: &[Pubkey], -) -> Result { +) -> 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)?, + ParsableProgram::SplToken => serde_json::to_value(parse_token(instruction, account_keys)?)?, }; - Ok(json!({ - format!("{:?}", program_name).to_kebab_case(): parsed_json - })) + Ok(ParsedInstruction { + program: format!("{:?}", program_name).to_kebab_case(), + program_id: program_id.to_string(), + parsed: parsed_json, + }) } fn parse_memo(instruction: &CompiledInstruction) -> Value { @@ -64,6 +85,7 @@ fn parse_memo(instruction: &CompiledInstruction) -> Value { #[cfg(test)] mod test { use super::*; + use serde_json::json; #[test] fn test_parse() { @@ -72,18 +94,16 @@ mod test { accounts: vec![], data: vec![240, 159, 166, 150], }; - let expected_json = json!({ - "spl-memo": "🦖" - }); assert_eq!( parse(&MEMO_PROGRAM_ID, &memo_instruction, &[]).unwrap(), - expected_json + ParsedInstruction { + program: "spl-memo".to_string(), + program_id: MEMO_PROGRAM_ID.to_string(), + parsed: json!("🦖"), + } ); let non_parsable_program_id = Pubkey::new(&[1; 32]); - assert_eq!( - parse(&non_parsable_program_id, &memo_instruction, &[]).unwrap_err(), - ParseInstructionError::ProgramNotParsable - ); + assert!(parse(&non_parsable_program_id, &memo_instruction, &[]).is_err()); } } diff --git a/transaction-status/src/parse_token.rs b/transaction-status/src/parse_token.rs index 92f0f99f7b..84db788b71 100644 --- a/transaction-status/src/parse_token.rs +++ b/transaction-status/src/parse_token.rs @@ -1,4 +1,4 @@ -use crate::parse_instruction::{ParsableProgram, ParseInstructionError}; +use crate::parse_instruction::{ParsableProgram, ParseInstructionError, ParsedInstructionEnum}; use serde_json::{json, Map, Value}; use solana_sdk::{instruction::CompiledInstruction, pubkey::Pubkey}; use spl_token_v1_0::instruction::TokenInstruction; @@ -6,7 +6,7 @@ use spl_token_v1_0::instruction::TokenInstruction; pub fn parse_token( instruction: &CompiledInstruction, account_keys: &[Pubkey], -) -> Result { +) -> Result { let token_instruction = TokenInstruction::unpack(&instruction.data) .map_err(|_| ParseInstructionError::InstructionNotParsable(ParsableProgram::SplToken))?; if instruction.accounts.len() > account_keys.len() { @@ -23,7 +23,6 @@ pub fn parse_token( )); } let mut value = json!({ - "type": "initializeMint", "mint": account_keys[instruction.accounts[0] as usize].to_string(), "amount": amount, "decimals":decimals, @@ -46,7 +45,10 @@ pub fn parse_token( ); } } - Ok(value) + Ok(ParsedInstructionEnum { + instruction_type: "initializeMint".to_string(), + info: value, + }) } TokenInstruction::InitializeAccount => { if instruction.accounts.len() < 3 { @@ -54,12 +56,14 @@ pub fn parse_token( 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(), - })) + Ok(ParsedInstructionEnum { + instruction_type: "initializeAccount".to_string(), + info: json!({ + "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 { @@ -71,12 +75,14 @@ pub fn parse_token( 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, - })) + Ok(ParsedInstructionEnum { + instruction_type: "initializeMultisig".to_string(), + info: json!({ + "multisig": account_keys[instruction.accounts[0] as usize].to_string(), + "signers": signers, + "m": m, + }), + }) } TokenInstruction::Transfer { amount } => { if instruction.accounts.len() < 3 { @@ -85,7 +91,6 @@ pub fn parse_token( )); } 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, @@ -99,7 +104,10 @@ pub fn parse_token( "authority", "multisigAuthority", ); - Ok(value) + Ok(ParsedInstructionEnum { + instruction_type: "transfer".to_string(), + info: value, + }) } TokenInstruction::Approve { amount } => { if instruction.accounts.len() < 3 { @@ -108,7 +116,6 @@ pub fn parse_token( )); } 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, @@ -122,7 +129,10 @@ pub fn parse_token( "owner", "multisigOwner", ); - Ok(value) + Ok(ParsedInstructionEnum { + instruction_type: "approve".to_string(), + info: value, + }) } TokenInstruction::Revoke => { if instruction.accounts.len() < 2 { @@ -131,7 +141,6 @@ pub fn parse_token( )); } let mut value = json!({ - "type": "revoke", "source": account_keys[instruction.accounts[0] as usize].to_string(), }); let mut map = value.as_object_mut().unwrap(); @@ -143,7 +152,10 @@ pub fn parse_token( "owner", "multisigOwner", ); - Ok(value) + Ok(ParsedInstructionEnum { + instruction_type: "revoke".to_string(), + info: value, + }) } TokenInstruction::SetOwner => { if instruction.accounts.len() < 3 { @@ -152,7 +164,6 @@ pub fn parse_token( )); } 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(), }); @@ -165,7 +176,10 @@ pub fn parse_token( "owner", "multisigOwner", ); - Ok(value) + Ok(ParsedInstructionEnum { + instruction_type: "setOwner".to_string(), + info: value, + }) } TokenInstruction::MintTo { amount } => { if instruction.accounts.len() < 3 { @@ -174,7 +188,6 @@ pub fn parse_token( )); } 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, @@ -188,7 +201,10 @@ pub fn parse_token( "owner", "multisigOwner", ); - Ok(value) + Ok(ParsedInstructionEnum { + instruction_type: "mintTo".to_string(), + info: value, + }) } TokenInstruction::Burn { amount } => { if instruction.accounts.len() < 2 { @@ -197,7 +213,6 @@ pub fn parse_token( )); } let mut value = json!({ - "type": "burn", "account": account_keys[instruction.accounts[0] as usize].to_string(), "amount": amount, }); @@ -210,7 +225,10 @@ pub fn parse_token( "authority", "multisigAuthority", ); - Ok(value) + Ok(ParsedInstructionEnum { + instruction_type: "burn".to_string(), + info: value, + }) } TokenInstruction::CloseAccount => { if instruction.accounts.len() < 3 { @@ -219,7 +237,6 @@ pub fn parse_token( )); } 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(), }); @@ -232,7 +249,10 @@ pub fn parse_token( "owner", "multisigOwner", ); - Ok(value) + Ok(ParsedInstructionEnum { + instruction_type: "closeAccount".to_string(), + info: value, + }) } } } @@ -311,14 +331,16 @@ mod test { 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(), - }) + ParsedInstructionEnum { + instruction_type: "initializeMint".to_string(), + info: json!({ + "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( @@ -334,13 +356,15 @@ mod test { 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(), - }) + ParsedInstructionEnum { + instruction_type: "initializeMint".to_string(), + info: json!({ + "mint": keys[0].to_string(), + "amount": 42, + "decimals": 2, + "account": keys[1].to_string(), + }) + } ); let initialize_mint_ix = initialize_mint( @@ -356,13 +380,15 @@ mod test { 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(), - }) + ParsedInstructionEnum { + instruction_type: "initializeMint".to_string(), + info: json!({ + "mint": keys[0].to_string(), + "amount": 0, + "decimals": 2, + "owner": keys[1].to_string(), + }) + } ); // Test InitializeAccount @@ -377,12 +403,14 @@ mod test { 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(), - }) + ParsedInstructionEnum { + instruction_type: "initializeAccount".to_string(), + info: json!({ + "account": keys[0].to_string(), + "mint": keys[1].to_string(), + "owner": keys[2].to_string(), + }) + } ); // Test InitializeMultisig @@ -401,12 +429,14 @@ mod test { 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::>(), - }) + ParsedInstructionEnum { + instruction_type: "initializeMultisig".to_string(), + info: json!({ + "multisig": keys[0].to_string(), + "m": 2, + "signers": keys[1..4].iter().map(|key| key.to_string()).collect::>(), + }) + } ); // Test Transfer, incl multisig @@ -423,13 +453,15 @@ mod test { 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, - }) + ParsedInstructionEnum { + instruction_type: "transfer".to_string(), + info: json!({ + "source": keys[1].to_string(), + "destination": keys[2].to_string(), + "authority": keys[0].to_string(), + "amount": 42, + }) + } ); let transfer_ix = transfer( @@ -445,14 +477,16 @@ mod test { 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, - }) + ParsedInstructionEnum { + instruction_type: "transfer".to_string(), + info: json!({ + "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 @@ -469,13 +503,15 @@ mod test { 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, - }) + ParsedInstructionEnum { + instruction_type: "approve".to_string(), + info: json!({ + "source": keys[1].to_string(), + "delegate": keys[2].to_string(), + "owner": keys[0].to_string(), + "amount": 42, + }) + } ); let approve_ix = approve( @@ -491,14 +527,16 @@ mod test { 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, - }) + ParsedInstructionEnum { + instruction_type: "approve".to_string(), + info: json!({ + "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 @@ -513,11 +551,13 @@ mod test { 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(), - }) + ParsedInstructionEnum { + instruction_type: "revoke".to_string(), + info: json!({ + "source": keys[1].to_string(), + "owner": keys[0].to_string(), + }) + } ); // Test SetOwner @@ -533,12 +573,14 @@ mod test { 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(), - }) + ParsedInstructionEnum { + instruction_type: "setOwner".to_string(), + info: json!({ + "owned": keys[1].to_string(), + "newOwner": keys[2].to_string(), + "owner": keys[0].to_string(), + }) + } ); // Test MintTo @@ -555,13 +597,15 @@ mod test { 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, - }) + ParsedInstructionEnum { + instruction_type: "mintTo".to_string(), + info: json!({ + "mint": keys[1].to_string(), + "account": keys[2].to_string(), + "owner": keys[0].to_string(), + "amount": 42, + }) + } ); // Test Burn @@ -577,12 +621,14 @@ mod test { 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, - }) + ParsedInstructionEnum { + instruction_type: "burn".to_string(), + info: json!({ + "account": keys[1].to_string(), + "authority": keys[0].to_string(), + "amount": 42, + }) + } ); // Test CloseAccount @@ -598,12 +644,14 @@ mod test { 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(), - }) + ParsedInstructionEnum { + instruction_type: "closeAccount".to_string(), + info: json!({ + "account": keys[1].to_string(), + "destination": keys[2].to_string(), + "owner": keys[0].to_string(), + }) + } ); }