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