solana/transaction-status/src/extract_memos.rs

149 lines
4.3 KiB
Rust

use {
crate::{parse_instruction::parse_memo_data, VersionedTransactionWithStatusMeta},
solana_sdk::{
instruction::CompiledInstruction,
message::{AccountKeys, Message, SanitizedMessage},
pubkey::Pubkey,
},
};
// A helper function to convert spl_memo::v1::id() as spl_sdk::pubkey::Pubkey to
// solana_sdk::pubkey::Pubkey
pub fn spl_memo_id_v1() -> Pubkey {
*MEMO_PROGRAM_ID_V1
}
// A helper function to convert spl_memo::id() as spl_sdk::pubkey::Pubkey to
// solana_sdk::pubkey::Pubkey
pub fn spl_memo_id_v3() -> Pubkey {
*MEMO_PROGRAM_ID_V3
}
lazy_static! {
static ref MEMO_PROGRAM_ID_V1: Pubkey = Pubkey::new_from_array(spl_memo::v1::id().to_bytes());
static ref MEMO_PROGRAM_ID_V3: Pubkey = Pubkey::new_from_array(spl_memo::id().to_bytes());
}
pub fn extract_and_fmt_memos<T: ExtractMemos>(message: &T) -> Option<String> {
let memos = message.extract_memos();
if memos.is_empty() {
None
} else {
Some(memos.join("; "))
}
}
fn extract_and_fmt_memo_data(data: &[u8]) -> String {
let memo_len = data.len();
let parsed_memo = parse_memo_data(data).unwrap_or_else(|_| "(unparseable)".to_string());
format!("[{memo_len}] {parsed_memo}")
}
pub trait ExtractMemos {
fn extract_memos(&self) -> Vec<String>;
}
impl ExtractMemos for Message {
fn extract_memos(&self) -> Vec<String> {
extract_memos_inner(
&AccountKeys::new(&self.account_keys, None),
&self.instructions,
)
}
}
impl ExtractMemos for SanitizedMessage {
fn extract_memos(&self) -> Vec<String> {
extract_memos_inner(&self.account_keys(), self.instructions())
}
}
impl ExtractMemos for VersionedTransactionWithStatusMeta {
fn extract_memos(&self) -> Vec<String> {
extract_memos_inner(
&self.account_keys(),
self.transaction.message.instructions(),
)
}
}
enum KeyType<'a> {
MemoProgram,
OtherProgram,
Unknown(&'a Pubkey),
}
fn extract_memos_inner(
account_keys: &AccountKeys,
instructions: &[CompiledInstruction],
) -> Vec<String> {
let mut account_keys: Vec<KeyType> = account_keys.iter().map(KeyType::Unknown).collect();
instructions
.iter()
.filter_map(|ix| {
let index = ix.program_id_index as usize;
let key_type = account_keys.get(index)?;
let memo_data = match key_type {
KeyType::MemoProgram => Some(&ix.data),
KeyType::OtherProgram => None,
KeyType::Unknown(program_id) => {
if **program_id == *MEMO_PROGRAM_ID_V1 || **program_id == *MEMO_PROGRAM_ID_V3 {
account_keys[index] = KeyType::MemoProgram;
Some(&ix.data)
} else {
account_keys[index] = KeyType::OtherProgram;
None
}
}
}?;
Some(extract_and_fmt_memo_data(memo_data))
})
.collect()
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_extract_memos_inner() {
let fee_payer = Pubkey::new_unique();
let another_program_id = Pubkey::new_unique();
let memo0 = "Test memo";
let memo1 = "🦖";
let expected_memos = vec![
format!("[{}] {}", memo0.len(), memo0),
format!("[{}] {}", memo1.len(), memo1),
];
let memo_instructions = vec![
CompiledInstruction {
program_id_index: 1,
accounts: vec![],
data: memo0.as_bytes().to_vec(),
},
CompiledInstruction {
program_id_index: 2,
accounts: vec![],
data: memo1.as_bytes().to_vec(),
},
CompiledInstruction {
program_id_index: 3,
accounts: vec![],
data: memo1.as_bytes().to_vec(),
},
];
let static_keys = vec![
fee_payer,
spl_memo_id_v1(),
another_program_id,
spl_memo_id_v3(),
];
let account_keys = AccountKeys::new(&static_keys, None);
assert_eq!(
extract_memos_inner(&account_keys, &memo_instructions),
expected_memos
);
}
}