solana_transaction_status/
extract_memos.rs

1use {
2    crate::{parse_instruction::parse_memo_data, VersionedTransactionWithStatusMeta},
3    solana_message::{
4        compiled_instruction::CompiledInstruction, AccountKeys, Message, SanitizedMessage,
5    },
6    solana_pubkey::Pubkey,
7};
8
9// A helper function to convert spl_memo::v1::id() as spl_sdk::pubkey::Pubkey to
10// solana_pubkey::Pubkey
11pub fn spl_memo_id_v1() -> Pubkey {
12    *MEMO_PROGRAM_ID_V1
13}
14
15// A helper function to convert spl_memo::id() as spl_sdk::pubkey::Pubkey to
16// solana_pubkey::Pubkey
17pub fn spl_memo_id_v3() -> Pubkey {
18    *MEMO_PROGRAM_ID_V3
19}
20
21lazy_static! {
22    static ref MEMO_PROGRAM_ID_V1: Pubkey = Pubkey::new_from_array(spl_memo::v1::id().to_bytes());
23    static ref MEMO_PROGRAM_ID_V3: Pubkey = Pubkey::new_from_array(spl_memo::id().to_bytes());
24}
25
26pub fn extract_and_fmt_memos<T: ExtractMemos>(message: &T) -> Option<String> {
27    let memos = message.extract_memos();
28    if memos.is_empty() {
29        None
30    } else {
31        Some(memos.join("; "))
32    }
33}
34
35fn extract_and_fmt_memo_data(data: &[u8]) -> String {
36    let memo_len = data.len();
37    let parsed_memo = parse_memo_data(data).unwrap_or_else(|_| "(unparseable)".to_string());
38    format!("[{memo_len}] {parsed_memo}")
39}
40
41pub trait ExtractMemos {
42    fn extract_memos(&self) -> Vec<String>;
43}
44
45impl ExtractMemos for Message {
46    fn extract_memos(&self) -> Vec<String> {
47        extract_memos_inner(
48            &AccountKeys::new(&self.account_keys, None),
49            &self.instructions,
50        )
51    }
52}
53
54impl ExtractMemos for SanitizedMessage {
55    fn extract_memos(&self) -> Vec<String> {
56        extract_memos_inner(&self.account_keys(), self.instructions())
57    }
58}
59
60impl ExtractMemos for VersionedTransactionWithStatusMeta {
61    fn extract_memos(&self) -> Vec<String> {
62        extract_memos_inner(
63            &self.account_keys(),
64            self.transaction.message.instructions(),
65        )
66    }
67}
68
69enum KeyType<'a> {
70    MemoProgram,
71    OtherProgram,
72    Unknown(&'a Pubkey),
73}
74
75fn extract_memos_inner(
76    account_keys: &AccountKeys,
77    instructions: &[CompiledInstruction],
78) -> Vec<String> {
79    let mut account_keys: Vec<KeyType> = account_keys.iter().map(KeyType::Unknown).collect();
80    instructions
81        .iter()
82        .filter_map(|ix| {
83            let index = ix.program_id_index as usize;
84            let key_type = account_keys.get(index)?;
85            let memo_data = match key_type {
86                KeyType::MemoProgram => Some(&ix.data),
87                KeyType::OtherProgram => None,
88                KeyType::Unknown(program_id) => {
89                    if **program_id == *MEMO_PROGRAM_ID_V1 || **program_id == *MEMO_PROGRAM_ID_V3 {
90                        account_keys[index] = KeyType::MemoProgram;
91                        Some(&ix.data)
92                    } else {
93                        account_keys[index] = KeyType::OtherProgram;
94                        None
95                    }
96                }
97            }?;
98            Some(extract_and_fmt_memo_data(memo_data))
99        })
100        .collect()
101}
102
103#[cfg(test)]
104mod test {
105    use super::*;
106
107    #[test]
108    fn test_extract_memos_inner() {
109        let fee_payer = Pubkey::new_unique();
110        let another_program_id = Pubkey::new_unique();
111        let memo0 = "Test memo";
112        let memo1 = "🦖";
113        let expected_memos = vec![
114            format!("[{}] {}", memo0.len(), memo0),
115            format!("[{}] {}", memo1.len(), memo1),
116        ];
117        let memo_instructions = vec![
118            CompiledInstruction {
119                program_id_index: 1,
120                accounts: vec![],
121                data: memo0.as_bytes().to_vec(),
122            },
123            CompiledInstruction {
124                program_id_index: 2,
125                accounts: vec![],
126                data: memo1.as_bytes().to_vec(),
127            },
128            CompiledInstruction {
129                program_id_index: 3,
130                accounts: vec![],
131                data: memo1.as_bytes().to_vec(),
132            },
133        ];
134        let static_keys = vec![
135            fee_payer,
136            spl_memo_id_v1(),
137            another_program_id,
138            spl_memo_id_v3(),
139        ];
140        let account_keys = AccountKeys::new(&static_keys, None);
141
142        assert_eq!(
143            extract_memos_inner(&account_keys, &memo_instructions),
144            expected_memos
145        );
146    }
147}