Add jsonParsed option for EncodedTransactions; add memo parser (#10711)
* Add jsonParsed option for EncodedTransactions; add memo parser * Use kebab case for program names * Add account-key parsing * Add parse test
This commit is contained in:
@@ -11,9 +11,13 @@ edition = "2018"
|
||||
[dependencies]
|
||||
bincode = "1.2.1"
|
||||
bs58 = "0.3.1"
|
||||
Inflector = "0.11.4"
|
||||
lazy_static = "1.4.0"
|
||||
solana-sdk = { path = "../sdk", version = "1.3.0" }
|
||||
spl-memo = "1.0.0"
|
||||
serde = "1.0.112"
|
||||
serde_derive = "1.0.103"
|
||||
serde_json = "1.0.54"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
||||
|
@@ -1,13 +1,28 @@
|
||||
#[macro_use]
|
||||
extern crate lazy_static;
|
||||
#[macro_use]
|
||||
extern crate serde_derive;
|
||||
|
||||
pub mod parse_accounts;
|
||||
pub mod parse_instruction;
|
||||
|
||||
use crate::{parse_accounts::parse_accounts, parse_instruction::parse};
|
||||
use serde_json::Value;
|
||||
use solana_sdk::{
|
||||
clock::Slot,
|
||||
commitment_config::CommitmentConfig,
|
||||
instruction::CompiledInstruction,
|
||||
message::MessageHeader,
|
||||
transaction::{Result, Transaction, TransactionError},
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", untagged)]
|
||||
pub enum RpcInstruction {
|
||||
Compiled(RpcCompiledInstruction),
|
||||
Parsed(Value),
|
||||
}
|
||||
|
||||
/// A duplicate representation of a Message for pretty JSON serialization
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -17,6 +32,16 @@ pub struct RpcCompiledInstruction {
|
||||
pub data: String,
|
||||
}
|
||||
|
||||
impl From<&CompiledInstruction> for RpcCompiledInstruction {
|
||||
fn from(instruction: &CompiledInstruction) -> Self {
|
||||
Self {
|
||||
program_id_index: instruction.program_id_index,
|
||||
accounts: instruction.accounts.clone(),
|
||||
data: bs58::encode(instruction.data.clone()).into_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct TransactionStatusMeta {
|
||||
@@ -109,16 +134,32 @@ pub struct RpcTransaction {
|
||||
pub message: RpcMessage,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", untagged)]
|
||||
pub enum RpcMessage {
|
||||
Parsed(RpcParsedMessage),
|
||||
Raw(RpcRawMessage),
|
||||
}
|
||||
|
||||
/// A duplicate representation of a Message for pretty JSON serialization
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RpcMessage {
|
||||
pub struct RpcRawMessage {
|
||||
pub header: MessageHeader,
|
||||
pub account_keys: Vec<String>,
|
||||
pub recent_blockhash: String,
|
||||
pub instructions: Vec<RpcCompiledInstruction>,
|
||||
}
|
||||
|
||||
/// A duplicate representation of a Message for pretty JSON serialization
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RpcParsedMessage {
|
||||
pub account_keys: Value,
|
||||
pub recent_blockhash: String,
|
||||
pub instructions: Vec<RpcInstruction>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct TransactionWithStatusMeta {
|
||||
@@ -131,6 +172,7 @@ pub struct TransactionWithStatusMeta {
|
||||
pub enum TransactionEncoding {
|
||||
Binary,
|
||||
Json,
|
||||
JsonParsed,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
@@ -142,38 +184,57 @@ pub enum EncodedTransaction {
|
||||
|
||||
impl EncodedTransaction {
|
||||
pub fn encode(transaction: Transaction, encoding: TransactionEncoding) -> Self {
|
||||
if encoding == TransactionEncoding::Json {
|
||||
EncodedTransaction::Json(RpcTransaction {
|
||||
signatures: transaction
|
||||
.signatures
|
||||
.iter()
|
||||
.map(|sig| sig.to_string())
|
||||
.collect(),
|
||||
message: RpcMessage {
|
||||
header: transaction.message.header,
|
||||
account_keys: transaction
|
||||
.message
|
||||
.account_keys
|
||||
.iter()
|
||||
.map(|pubkey| pubkey.to_string())
|
||||
.collect(),
|
||||
recent_blockhash: transaction.message.recent_blockhash.to_string(),
|
||||
instructions: transaction
|
||||
.message
|
||||
.instructions
|
||||
.iter()
|
||||
.map(|instruction| RpcCompiledInstruction {
|
||||
program_id_index: instruction.program_id_index,
|
||||
accounts: instruction.accounts.clone(),
|
||||
data: bs58::encode(instruction.data.clone()).into_string(),
|
||||
})
|
||||
.collect(),
|
||||
},
|
||||
})
|
||||
} else {
|
||||
EncodedTransaction::Binary(
|
||||
match encoding {
|
||||
TransactionEncoding::Binary => EncodedTransaction::Binary(
|
||||
bs58::encode(bincode::serialize(&transaction).unwrap()).into_string(),
|
||||
)
|
||||
),
|
||||
_ => {
|
||||
let message = if encoding == TransactionEncoding::Json {
|
||||
RpcMessage::Raw(RpcRawMessage {
|
||||
header: transaction.message.header,
|
||||
account_keys: transaction
|
||||
.message
|
||||
.account_keys
|
||||
.iter()
|
||||
.map(|pubkey| pubkey.to_string())
|
||||
.collect(),
|
||||
recent_blockhash: transaction.message.recent_blockhash.to_string(),
|
||||
instructions: transaction
|
||||
.message
|
||||
.instructions
|
||||
.iter()
|
||||
.map(|instruction| instruction.into())
|
||||
.collect(),
|
||||
})
|
||||
} else {
|
||||
RpcMessage::Parsed(RpcParsedMessage {
|
||||
account_keys: parse_accounts(&transaction.message),
|
||||
recent_blockhash: transaction.message.recent_blockhash.to_string(),
|
||||
instructions: transaction
|
||||
.message
|
||||
.instructions
|
||||
.iter()
|
||||
.map(|instruction| {
|
||||
let program_id =
|
||||
instruction.program_id(&transaction.message.account_keys);
|
||||
if let Some(parsed_instruction) = parse(program_id, instruction) {
|
||||
RpcInstruction::Parsed(parsed_instruction)
|
||||
} else {
|
||||
RpcInstruction::Compiled(instruction.into())
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
};
|
||||
EncodedTransaction::Json(RpcTransaction {
|
||||
signatures: transaction
|
||||
.signatures
|
||||
.iter()
|
||||
.map(|sig| sig.to_string())
|
||||
.collect(),
|
||||
message,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn decode(&self) -> Option<Transaction> {
|
||||
|
56
transaction-status/src/parse_accounts.rs
Normal file
56
transaction-status/src/parse_accounts.rs
Normal file
@@ -0,0 +1,56 @@
|
||||
use serde_json::{json, Value};
|
||||
use solana_sdk::message::Message;
|
||||
|
||||
type AccountAttributes = Vec<AccountAttribute>;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
enum AccountAttribute {
|
||||
Signer,
|
||||
Writable,
|
||||
}
|
||||
|
||||
pub fn parse_accounts(message: &Message) -> Value {
|
||||
let mut accounts: Vec<Value> = 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.push(json!({ account_key.to_string(): attributes }));
|
||||
}
|
||||
json!(accounts)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use solana_sdk::{message::MessageHeader, pubkey::Pubkey};
|
||||
|
||||
#[test]
|
||||
fn test_parse_accounts() {
|
||||
let pubkey0 = Pubkey::new_rand();
|
||||
let pubkey1 = Pubkey::new_rand();
|
||||
let pubkey2 = Pubkey::new_rand();
|
||||
let pubkey3 = Pubkey::new_rand();
|
||||
let mut message = Message::default();
|
||||
message.header = MessageHeader {
|
||||
num_required_signatures: 2,
|
||||
num_readonly_signed_accounts: 1,
|
||||
num_readonly_unsigned_accounts: 1,
|
||||
};
|
||||
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);
|
||||
}
|
||||
}
|
59
transaction-status/src/parse_instruction.rs
Normal file
59
transaction-status/src/parse_instruction.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
use inflector::Inflector;
|
||||
use serde_json::{json, Value};
|
||||
use solana_sdk::{instruction::CompiledInstruction, pubkey::Pubkey};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
str::{from_utf8, FromStr},
|
||||
};
|
||||
|
||||
lazy_static! {
|
||||
static ref MEMO_PROGRAM_ID: Pubkey = Pubkey::from_str(&spl_memo::id().to_string()).unwrap();
|
||||
pub static ref PARSABLE_PROGRAM_IDS: HashMap<Pubkey, ParsableProgram> = {
|
||||
let mut m = HashMap::new();
|
||||
m.insert(*MEMO_PROGRAM_ID, ParsableProgram::SplMemo);
|
||||
m
|
||||
};
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum ParsableProgram {
|
||||
SplMemo,
|
||||
}
|
||||
|
||||
pub fn parse(program_id: &Pubkey, instruction: &CompiledInstruction) -> Option<Value> {
|
||||
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 })
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_memo(instruction: &CompiledInstruction) -> Value {
|
||||
Value::String(from_utf8(&instruction.data).unwrap().to_string())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_parse() {
|
||||
let memo_instruction = CompiledInstruction {
|
||||
program_id_index: 0,
|
||||
accounts: vec![],
|
||||
data: vec![240, 159, 166, 150],
|
||||
};
|
||||
let expected_json = json!({
|
||||
"spl-memo": "🦖"
|
||||
});
|
||||
assert_eq!(
|
||||
parse(&MEMO_PROGRAM_ID, &memo_instruction),
|
||||
Some(expected_json)
|
||||
);
|
||||
|
||||
let non_parsable_program_id = Pubkey::new(&[1; 32]);
|
||||
assert_eq!(parse(&non_parsable_program_id, &memo_instruction), None);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user