From fd2508b09ea7c0d2dbb2b9019dedaef3042508f8 Mon Sep 17 00:00:00 2001 From: Tyera Eulberg Date: Fri, 19 Jun 2020 16:15:13 -0600 Subject: [PATCH] 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 --- Cargo.lock | 149 +++++++++++++------- core/src/rpc.rs | 8 +- transaction-status/Cargo.toml | 4 + transaction-status/src/lib.rs | 125 +++++++++++----- transaction-status/src/parse_accounts.rs | 56 ++++++++ transaction-status/src/parse_instruction.rs | 59 ++++++++ 6 files changed, 317 insertions(+), 84 deletions(-) create mode 100644 transaction-status/src/parse_accounts.rs create mode 100644 transaction-status/src/parse_instruction.rs diff --git a/Cargo.lock b/Cargo.lock index b1590c8bbd..1ff7dc36cb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3641,7 +3641,7 @@ dependencies = [ "solana-logger", "solana-measure", "solana-runtime", - "solana-sdk", + "solana-sdk 1.3.0", ] [[package]] @@ -3660,7 +3660,7 @@ dependencies = [ "solana-measure", "solana-perf", "solana-runtime", - "solana-sdk", + "solana-sdk 1.3.0", "solana-streamer", "solana-version", ] @@ -3689,7 +3689,7 @@ dependencies = [ "solana-metrics", "solana-net-utils", "solana-runtime", - "solana-sdk", + "solana-sdk 1.3.0", "solana-version", ] @@ -3730,7 +3730,7 @@ dependencies = [ "solana-move-loader-program", "solana-net-utils", "solana-runtime", - "solana-sdk", + "solana-sdk 1.3.0", "solana-version", ] @@ -3745,7 +3745,7 @@ dependencies = [ "num-traits", "rand 0.7.3", "solana-runtime", - "solana-sdk", + "solana-sdk 1.3.0", "solana_rbpf", "thiserror", ] @@ -3761,7 +3761,7 @@ dependencies = [ "num-traits", "serde", "serde_derive", - "solana-sdk", + "solana-sdk 1.3.0", ] [[package]] @@ -3776,7 +3776,7 @@ dependencies = [ "serde", "serde_derive", "solana-runtime", - "solana-sdk", + "solana-sdk 1.3.0", "thiserror", ] @@ -3788,7 +3788,7 @@ dependencies = [ "clap", "rpassword", "solana-remote-wallet", - "solana-sdk", + "solana-sdk 1.3.0", "thiserror", "tiny-bip39", "url 2.1.1", @@ -3827,7 +3827,7 @@ dependencies = [ "solana-net-utils", "solana-remote-wallet", "solana-runtime", - "solana-sdk", + "solana-sdk 1.3.0", "solana-stake-program", "solana-transaction-status", "solana-version", @@ -3868,7 +3868,7 @@ dependencies = [ "serde_json", "solana-logger", "solana-net-utils", - "solana-sdk", + "solana-sdk 1.3.0", "solana-transaction-status", "solana-vote-program", "thiserror", @@ -3886,7 +3886,7 @@ dependencies = [ "serde", "serde_derive", "solana-logger", - "solana-sdk", + "solana-sdk 1.3.0", ] [[package]] @@ -3940,7 +3940,7 @@ dependencies = [ "solana-perf", "solana-rayon-threadlimit", "solana-runtime", - "solana-sdk", + "solana-sdk 1.3.0", "solana-stake-program", "solana-streamer", "solana-sys-tuner", @@ -3996,7 +3996,7 @@ dependencies = [ "solana-logger", "solana-net-utils", "solana-runtime", - "solana-sdk", + "solana-sdk 1.3.0", "solana-version", ] @@ -4010,7 +4010,7 @@ dependencies = [ "log 0.4.8", "reqwest", "solana-runtime", - "solana-sdk", + "solana-sdk 1.3.0", "tar", ] @@ -4027,7 +4027,7 @@ dependencies = [ "solana-logger", "solana-metrics", "solana-runtime", - "solana-sdk", + "solana-sdk 1.3.0", "thiserror", ] @@ -4036,7 +4036,7 @@ name = "solana-failure-program" version = "1.3.0" dependencies = [ "solana-runtime", - "solana-sdk", + "solana-sdk 1.3.0", ] [[package]] @@ -4053,7 +4053,7 @@ dependencies = [ "solana-clap-utils", "solana-logger", "solana-metrics", - "solana-sdk", + "solana-sdk 1.3.0", "solana-version", "tokio 0.1.22", "tokio-codec", @@ -4074,7 +4074,7 @@ dependencies = [ "solana-ledger", "solana-logger", "solana-runtime", - "solana-sdk", + "solana-sdk 1.3.0", "solana-stake-program", "solana-version", "solana-vote-program", @@ -4090,7 +4090,7 @@ dependencies = [ "solana-budget-program", "solana-exchange-program", "solana-runtime", - "solana-sdk", + "solana-sdk 1.3.0", "solana-vest-program", ] @@ -4104,7 +4104,7 @@ dependencies = [ "solana-core", "solana-logger", "solana-net-utils", - "solana-sdk", + "solana-sdk 1.3.0", "solana-version", ] @@ -4132,7 +4132,7 @@ dependencies = [ "solana-client", "solana-config-program", "solana-logger", - "solana-sdk", + "solana-sdk 1.3.0", "solana-version", "tar", "tempdir", @@ -4152,7 +4152,7 @@ dependencies = [ "solana-clap-utils", "solana-cli-config", "solana-remote-wallet", - "solana-sdk", + "solana-sdk 1.3.0", "solana-version", "tiny-bip39", ] @@ -4193,7 +4193,7 @@ dependencies = [ "solana-perf", "solana-rayon-threadlimit", "solana-runtime", - "solana-sdk", + "solana-sdk 1.3.0", "solana-stake-program", "solana-transaction-status", "solana-vote-program", @@ -4218,7 +4218,7 @@ dependencies = [ "solana-ledger", "solana-logger", "solana-runtime", - "solana-sdk", + "solana-sdk 1.3.0", "solana-stake-program", "solana-transaction-status", "solana-version", @@ -4235,7 +4235,7 @@ dependencies = [ "solana-logger", "solana-move-loader-program", "solana-runtime", - "solana-sdk", + "solana-sdk 1.3.0", "solana_libra_types", ] @@ -4260,7 +4260,7 @@ dependencies = [ "solana-logger", "solana-rayon-threadlimit", "solana-runtime", - "solana-sdk", + "solana-sdk 1.3.0", "solana-stake-program", "solana-vest-program", "solana-vote-program", @@ -4297,7 +4297,7 @@ dependencies = [ "jemallocator", "log 0.4.8", "solana-metrics", - "solana-sdk", + "solana-sdk 1.3.0", ] [[package]] @@ -4306,7 +4306,7 @@ version = "1.3.0" dependencies = [ "fast-math", "hex 0.4.2", - "solana-sdk", + "solana-sdk 1.3.0", ] [[package]] @@ -4321,7 +4321,7 @@ dependencies = [ "reqwest", "serial_test", "serial_test_derive", - "solana-sdk", + "solana-sdk 1.3.0", ] [[package]] @@ -4338,7 +4338,7 @@ dependencies = [ "serde_derive", "serde_json", "solana-logger", - "solana-sdk", + "solana-sdk 1.3.0", "solana_libra_bytecode_verifier", "solana_libra_canonical_serialization", "solana_libra_compiler", @@ -4391,7 +4391,7 @@ version = "1.3.0" dependencies = [ "log 0.4.8", "solana-logger", - "solana-sdk", + "solana-sdk 1.3.0", ] [[package]] @@ -4411,7 +4411,7 @@ dependencies = [ "num-derive 0.3.0", "num-traits", "solana-runtime", - "solana-sdk", + "solana-sdk 1.3.0", "thiserror", ] @@ -4433,7 +4433,7 @@ dependencies = [ "solana-logger", "solana-metrics", "solana-rayon-threadlimit", - "solana-sdk", + "solana-sdk 1.3.0", ] [[package]] @@ -4453,7 +4453,7 @@ dependencies = [ "solana-metrics", "solana-net-utils", "solana-notifier", - "solana-sdk", + "solana-sdk 1.3.0", "solana-stake-program", "tar", ] @@ -4479,7 +4479,7 @@ dependencies = [ "num-traits", "parking_lot 0.10.2", "semver", - "solana-sdk", + "solana-sdk 1.3.0", "thiserror", "url 2.1.1", ] @@ -4517,7 +4517,7 @@ dependencies = [ "solana-metrics", "solana-noop-program", "solana-rayon-threadlimit", - "solana-sdk", + "solana-sdk 1.3.0", "solana-stake-program", "solana-vote-program", "symlink", @@ -4535,6 +4535,30 @@ dependencies = [ "serde", ] +[[package]] +name = "solana-sdk" +version = "1.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14b628fa500e0b83df3e96f7cc21dc998d8841a994f1c2109475273e6448afd4" +dependencies = [ + "bincode", + "bs58 0.3.1", + "bv", + "hex 0.4.2", + "hmac", + "itertools 0.9.0", + "log 0.4.8", + "num-derive 0.3.0", + "num-traits", + "pbkdf2", + "serde", + "serde_bytes", + "serde_derive", + "sha2", + "solana-sdk-macro 1.2.4", + "thiserror", +] + [[package]] name = "solana-sdk" version = "1.3.0" @@ -4565,12 +4589,24 @@ dependencies = [ "sha2", "solana-crate-features", "solana-logger", - "solana-sdk-macro", + "solana-sdk-macro 1.3.0", "solana-sdk-macro-frozen-abi", "thiserror", "tiny-bip39", ] +[[package]] +name = "solana-sdk-macro" +version = "1.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da5f311e7735323eb0ad348c68170c2503a2c56cfa1a261646d8182b373fa670" +dependencies = [ + "bs58 0.3.1", + "proc-macro2 1.0.17", + "quote 1.0.6", + "syn 1.0.27", +] + [[package]] name = "solana-sdk-macro" version = "1.3.0" @@ -4602,7 +4638,7 @@ dependencies = [ "solana-client", "solana-remote-wallet", "solana-runtime", - "solana-sdk", + "solana-sdk 1.3.0", "solana-stake-program", ] @@ -4624,7 +4660,7 @@ dependencies = [ "solana-local-cluster", "solana-logger", "solana-metrics", - "solana-sdk", + "solana-sdk 1.3.0", "solana-stake-program", "solana-transaction-status", "solana-version", @@ -4645,7 +4681,7 @@ dependencies = [ "solana-logger", "solana-metrics", "solana-notifier", - "solana-sdk", + "solana-sdk 1.3.0", "solana-stake-program", "solana-transaction-status", ] @@ -4663,7 +4699,7 @@ dependencies = [ "solana-config-program", "solana-logger", "solana-metrics", - "solana-sdk", + "solana-sdk 1.3.0", "solana-vote-program", "thiserror", ] @@ -4679,7 +4715,7 @@ dependencies = [ "solana-measure", "solana-metrics", "solana-perf", - "solana-sdk", + "solana-sdk 1.3.0", "thiserror", ] @@ -4718,7 +4754,7 @@ dependencies = [ "solana-core", "solana-remote-wallet", "solana-runtime", - "solana-sdk", + "solana-sdk 1.3.0", "solana-stake-program", "solana-transaction-status", "tempfile", @@ -4729,11 +4765,15 @@ dependencies = [ name = "solana-transaction-status" version = "1.3.0" dependencies = [ + "Inflector", "bincode", "bs58 0.3.1", + "lazy_static", "serde", "serde_derive", - "solana-sdk", + "serde_json", + "solana-sdk 1.3.0", + "spl-memo", ] [[package]] @@ -4767,7 +4807,7 @@ dependencies = [ "solana-net-utils", "solana-perf", "solana-runtime", - "solana-sdk", + "solana-sdk 1.3.0", "solana-version", "solana-vote-program", "solana-vote-signer", @@ -4779,7 +4819,7 @@ version = "1.3.0" dependencies = [ "serde", "serde_derive", - "solana-sdk", + "solana-sdk 1.3.0", ] [[package]] @@ -4794,7 +4834,7 @@ dependencies = [ "serde_derive", "solana-config-program", "solana-runtime", - "solana-sdk", + "solana-sdk 1.3.0", "thiserror", ] @@ -4809,7 +4849,7 @@ dependencies = [ "serde", "serde_derive", "solana-metrics", - "solana-sdk", + "solana-sdk 1.3.0", "thiserror", ] @@ -4825,7 +4865,7 @@ dependencies = [ "serde_json", "solana-clap-utils", "solana-metrics", - "solana-sdk", + "solana-sdk 1.3.0", "solana-version", ] @@ -4843,7 +4883,7 @@ dependencies = [ "solana-logger", "solana-metrics", "solana-notifier", - "solana-sdk", + "solana-sdk 1.3.0", "solana-transaction-status", "solana-version", "solana-vote-program", @@ -5216,6 +5256,15 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" +[[package]] +name = "spl-memo" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e6b954ac8b1df3f0bbb6ad1f21607be304f3cc9914bb9107c44b2065c8479e" +dependencies = [ + "solana-sdk 1.2.4", +] + [[package]] name = "stable_deref_trait" version = "1.1.1" diff --git a/core/src/rpc.rs b/core/src/rpc.rs index 03420cc84d..0a04c61962 100644 --- a/core/src/rpc.rs +++ b/core/src/rpc.rs @@ -1634,7 +1634,7 @@ pub mod tests { system_transaction, transaction::{self, TransactionError}, }; - use solana_transaction_status::{EncodedTransaction, TransactionWithStatusMeta}; + use solana_transaction_status::{EncodedTransaction, RpcMessage, TransactionWithStatusMeta}; use solana_vote_program::{ vote_instruction, vote_state::{Vote, VoteInit, MAX_LOCKOUT_HISTORY}, @@ -3209,7 +3209,11 @@ pub mod tests { if let EncodedTransaction::Json(transaction) = transaction { if transaction.signatures[0] == confirmed_block_signatures[0].to_string() { let meta = meta.unwrap(); - assert_eq!(transaction.message.recent_blockhash, blockhash.to_string()); + let transaction_recent_blockhash = match transaction.message { + RpcMessage::Parsed(message) => message.recent_blockhash, + RpcMessage::Raw(message) => message.recent_blockhash, + }; + assert_eq!(transaction_recent_blockhash, blockhash.to_string()); assert_eq!(meta.status, Ok(())); assert_eq!(meta.err, None); } else if transaction.signatures[0] == confirmed_block_signatures[1].to_string() { diff --git a/transaction-status/Cargo.toml b/transaction-status/Cargo.toml index 04290cbb84..59a0db4c98 100644 --- a/transaction-status/Cargo.toml +++ b/transaction-status/Cargo.toml @@ -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"] diff --git a/transaction-status/src/lib.rs b/transaction-status/src/lib.rs index fb42d20748..2d8fe079b2 100644 --- a/transaction-status/src/lib.rs +++ b/transaction-status/src/lib.rs @@ -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, pub recent_blockhash: String, pub instructions: Vec, } +/// 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, +} + #[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 { diff --git a/transaction-status/src/parse_accounts.rs b/transaction-status/src/parse_accounts.rs new file mode 100644 index 0000000000..71ef7bf91e --- /dev/null +++ b/transaction-status/src/parse_accounts.rs @@ -0,0 +1,56 @@ +use serde_json::{json, Value}; +use solana_sdk::message::Message; + +type AccountAttributes = Vec; + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +enum AccountAttribute { + Signer, + Writable, +} + +pub fn parse_accounts(message: &Message) -> Value { + 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.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); + } +} diff --git a/transaction-status/src/parse_instruction.rs b/transaction-status/src/parse_instruction.rs new file mode 100644 index 0000000000..e971735a42 --- /dev/null +++ b/transaction-status/src/parse_instruction.rs @@ -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 = { + 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 { + 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); + } +}