Update instruction encoding format (#11363)

* Rework parsed instruction format

* Rework parsed message accounts

* Review comments
This commit is contained in:
Tyera Eulberg 2020-08-05 00:58:58 -06:00 committed by GitHub
parent 86e3f96f16
commit 9d4f9be1fe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 264 additions and 173 deletions

View File

@ -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<ParsedAccount>,
pub recent_blockhash: String,
pub instructions: Vec<UiInstruction>,
}
@ -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,
),
))
}
})

View File

@ -1,28 +1,23 @@
use serde_json::{json, Map, Value};
use solana_sdk::message::Message;
type AccountAttributes = Vec<AccountAttribute>;
#[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<String, Value> = Map::new();
pub fn parse_accounts(message: &Message) -> Vec<ParsedAccount> {
let mut accounts: Vec<ParsedAccount> = 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,
},
]
);
}
}

View File

@ -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<Value, ParseInstructionError> {
) -> Result<ParsedInstruction, ParseInstructionError> {
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());
}
}

View File

@ -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<Value, ParseInstructionError> {
) -> Result<ParsedInstructionEnum, ParseInstructionError> {
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::<Vec<String>>(),
})
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::<Vec<String>>(),
})
}
);
// 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::<Vec<String>>(),
"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::<Vec<String>>(),
"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::<Vec<String>>(),
"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::<Vec<String>>(),
"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(),
})
}
);
}