1use std::collections::{HashMap, HashSet};
2
3use anchor_lang::prelude::{event::EVENT_IX_TAG_LE, AccountMeta};
4use solana_sdk::{
5 instruction::CompiledInstruction, message::v0::MessageAddressTableLookup, pubkey::Pubkey,
6 signature::Signature, transaction::VersionedTransaction,
7};
8use solana_transaction_status_client_types::{
9 option_serializer::OptionSerializer, EncodedTransactionWithStatusMeta, UiInstruction,
10 UiTransactionStatusMeta,
11};
12
13use crate::{Decode, DecodeError, Decoder, Visitor};
14
15pub use solana_transaction_status_client_types as solana_transaction_status;
16
17pub struct TransactionDecoder<'a> {
19 slot: u64,
20 signature: Signature,
21 transaction: &'a EncodedTransactionWithStatusMeta,
22 cpi_event_filter: CPIEventFilter,
23}
24
25impl<'a> TransactionDecoder<'a> {
26 pub fn new(
28 slot: u64,
29 signature: Signature,
30 transaction: &'a EncodedTransactionWithStatusMeta,
31 ) -> Self {
32 Self {
33 slot,
34 signature,
35 transaction,
36 cpi_event_filter: CPIEventFilter {
37 map: Default::default(),
38 },
39 }
40 }
41
42 pub fn add_cpi_event_program_id(
44 &mut self,
45 program_id: &Pubkey,
46 ) -> Result<&mut Self, DecodeError> {
47 self.cpi_event_filter.add(program_id)?;
48 Ok(self)
49 }
50
51 pub fn add_cpi_event_authority_and_program_id(
53 &mut self,
54 event_authority: Pubkey,
55 program_id: Pubkey,
56 ) -> Result<&mut Self, DecodeError> {
57 self.cpi_event_filter
58 .add_event_authority_and_program_id(event_authority, program_id)?;
59 Ok(self)
60 }
61
62 pub fn set_cpi_event_filter(&mut self, filter: CPIEventFilter) -> &mut Self {
64 self.cpi_event_filter = filter;
65 self
66 }
67
68 pub fn signature(&self) -> Signature {
70 self.signature
71 }
72
73 pub fn slot(&self) -> u64 {
75 self.slot
76 }
77
78 pub fn transaction(&self) -> &EncodedTransactionWithStatusMeta {
80 self.transaction
81 }
82
83 pub fn decoded_transaction(&self) -> Result<DecodedTransaction, DecodeError> {
85 let tx = self.transaction;
86 let slot_index = (self.slot, None);
87 let Some(decoded) = tx.transaction.decode() else {
88 return Err(DecodeError::custom("failed to decode transaction"));
89 };
90 let Some(meta) = &tx.meta else {
91 return Err(DecodeError::custom("missing meta"));
92 };
93
94 let (dynamic_writable_accounts, dynamic_readonly_accounts) = match &meta.loaded_addresses {
95 OptionSerializer::Some(loaded) => {
96 let dynamic_writable_accounts = loaded
97 .writable
98 .iter()
99 .map(|address| address.parse().map_err(DecodeError::custom))
100 .collect::<Result<Vec<_>, _>>()?;
101 let dynamic_readonly_accounts = loaded
102 .readonly
103 .iter()
104 .map(|address| address.parse().map_err(DecodeError::custom))
105 .collect::<Result<Vec<_>, _>>()?;
106 (dynamic_writable_accounts, dynamic_readonly_accounts)
107 }
108 OptionSerializer::None | OptionSerializer::Skip => Default::default(),
109 };
110
111 Ok(DecodedTransaction {
112 signature: self.signature,
113 slot_index,
114 transaction: decoded,
115 dynamic_writable_accounts,
116 dynamic_readonly_accounts,
117 transaction_status_meta: meta,
118 })
119 }
120
121 pub fn extract_cpi_events(&self) -> Result<CPIEvents, DecodeError> {
123 self.decoded_transaction()?
124 .extract_cpi_events(&self.cpi_event_filter)
125 }
126}
127
128impl Decoder for TransactionDecoder<'_> {
129 fn decode_account<V>(&self, _visitor: V) -> Result<V::Value, DecodeError>
130 where
131 V: Visitor,
132 {
133 Err(DecodeError::custom(
134 "Expecting `Account` but found `Transaction`",
135 ))
136 }
137
138 fn decode_transaction<V>(&self, visitor: V) -> Result<V::Value, DecodeError>
139 where
140 V: Visitor,
141 {
142 visitor.visit_transaction(self.decoded_transaction()?)
143 }
144
145 fn decode_anchor_cpi_events<V>(&self, visitor: V) -> Result<V::Value, DecodeError>
146 where
147 V: Visitor,
148 {
149 visitor.visit_anchor_cpi_events(self.extract_cpi_events()?.access())
150 }
151
152 fn decode_owned_data<V>(&self, _visitor: V) -> Result<V::Value, DecodeError>
153 where
154 V: Visitor,
155 {
156 Err(DecodeError::custom(
157 "cannot access ownedd data directly of a transaction",
158 ))
159 }
160
161 fn decode_bytes<V>(&self, _visitor: V) -> Result<V::Value, DecodeError>
162 where
163 V: Visitor,
164 {
165 Err(DecodeError::custom(
166 "cannot access bytes directly of a transaction",
167 ))
168 }
169}
170
171#[derive(Debug, Clone, Default)]
173pub struct CPIEventFilter {
174 map: HashMap<Pubkey, Pubkey>,
176}
177
178impl CPIEventFilter {
179 pub fn add(&mut self, program_id: &Pubkey) -> Result<&mut Self, DecodeError> {
181 let event_authority = find_event_authority_address(program_id);
182 self.add_event_authority_and_program_id(event_authority, *program_id)
183 }
184
185 pub fn add_event_authority_and_program_id(
187 &mut self,
188 event_authority: Pubkey,
189 program_id: Pubkey,
190 ) -> Result<&mut Self, DecodeError> {
191 if let Some(previous) = self.map.insert(event_authority, program_id) {
192 if previous != program_id {
194 return Err(DecodeError::custom(format!(
195 "event authority collision, previous={previous}, current={program_id}"
196 )));
197 }
198 }
199 Ok(self)
200 }
201
202 pub fn event_authorities(&self) -> impl Iterator<Item = &Pubkey> {
204 self.map.keys()
205 }
206
207 pub fn programs(&self) -> impl Iterator<Item = &Pubkey> {
209 self.map.values()
210 }
211}
212
213pub struct CPIEvent {
215 program_id: Pubkey,
216 data: Vec<u8>,
217}
218
219impl CPIEvent {
220 pub fn new(program_id: Pubkey, data: Vec<u8>) -> Self {
222 Self { program_id, data }
223 }
224}
225
226impl Decoder for &CPIEvent {
227 fn decode_account<V>(&self, _visitor: V) -> Result<V::Value, DecodeError>
228 where
229 V: Visitor,
230 {
231 Err(DecodeError::InvalidType(
232 "Expecting `Account` but found `CPIEvent`".to_string(),
233 ))
234 }
235
236 fn decode_transaction<V>(&self, _visitor: V) -> Result<V::Value, DecodeError>
237 where
238 V: Visitor,
239 {
240 Err(DecodeError::InvalidType(
241 "Expecting `Transaction` but found `CPIEvent`".to_string(),
242 ))
243 }
244
245 fn decode_anchor_cpi_events<V>(&self, _visitor: V) -> Result<V::Value, DecodeError>
246 where
247 V: Visitor,
248 {
249 Err(DecodeError::InvalidType(
250 "Expecting `AnchorCPIEvents` but found `CPIEvent`".to_string(),
251 ))
252 }
253
254 fn decode_owned_data<V>(&self, visitor: V) -> Result<V::Value, DecodeError>
255 where
256 V: Visitor,
257 {
258 visitor.visit_owned_data(&self.program_id, &self.data)
259 }
260
261 fn decode_bytes<V>(&self, visitor: V) -> Result<V::Value, DecodeError>
262 where
263 V: Visitor,
264 {
265 visitor.visit_bytes(&self.data)
266 }
267}
268
269pub type SlotAndIndex = (u64, Option<usize>);
271
272pub struct CPIEvents {
274 pub signature: Signature,
276 pub slot_index: SlotAndIndex,
278 pub events: Vec<CPIEvent>,
280}
281
282impl CPIEvents {
283 pub fn access(&self) -> AccessCPIEvents {
285 AccessCPIEvents {
286 signature: &self.signature,
287 slot_index: &self.slot_index,
288 events: self.events.iter(),
289 }
290 }
291}
292
293pub struct AccessCPIEvents<'a> {
295 signature: &'a Signature,
296 slot_index: &'a SlotAndIndex,
297 events: std::slice::Iter<'a, CPIEvent>,
298}
299
300impl<'a> AccessCPIEvents<'a> {
301 pub fn new(
303 signature: &'a Signature,
304 slot_index: &'a SlotAndIndex,
305 events: &'a [CPIEvent],
306 ) -> Self {
307 Self {
308 signature,
309 slot_index,
310 events: events.iter(),
311 }
312 }
313}
314
315impl<'a> crate::AnchorCPIEventsAccess<'a> for AccessCPIEvents<'a> {
316 fn slot(&self) -> Result<u64, DecodeError> {
317 Ok(self.slot_index.0)
318 }
319
320 fn index(&self) -> Result<Option<usize>, DecodeError> {
321 Ok(self.slot_index.1)
322 }
323
324 fn signature(&self) -> Result<&Signature, DecodeError> {
325 Ok(self.signature)
326 }
327
328 fn next_event<T>(&mut self) -> Result<Option<T>, DecodeError>
329 where
330 T: Decode,
331 {
332 let Some(decoder) = self.events.next() else {
333 return Ok(None);
334 };
335 T::decode(decoder).map(Some)
336 }
337}
338
339const EVENT_AUTHORITY_SEED: &[u8] = b"__event_authority";
340
341fn find_event_authority_address(program_id: &Pubkey) -> Pubkey {
342 Pubkey::find_program_address(&[EVENT_AUTHORITY_SEED], program_id).0
343}
344
345pub struct DecodedTransaction<'a> {
347 pub signature: Signature,
349 pub slot_index: SlotAndIndex,
351 pub transaction: VersionedTransaction,
353 pub dynamic_writable_accounts: Vec<Pubkey>,
355 pub dynamic_readonly_accounts: Vec<Pubkey>,
357 pub transaction_status_meta: &'a UiTransactionStatusMeta,
359}
360
361impl DecodedTransaction<'_> {
362 pub fn extract_cpi_events(
364 &self,
365 cpi_event_filter: &CPIEventFilter,
366 ) -> Result<CPIEvents, DecodeError> {
367 let mut event_authority_indices = HashMap::<_, HashSet<u8>>::default();
368 let mut accounts = self.transaction.message.static_account_keys().to_vec();
369 accounts.extend_from_slice(&self.dynamic_writable_accounts);
370 accounts.extend_from_slice(&self.dynamic_readonly_accounts);
371 tracing::debug!("accounts: {accounts:#?}");
372 let map = &cpi_event_filter.map;
373 for res in accounts
374 .iter()
375 .enumerate()
376 .filter(|(_, key)| map.contains_key(key))
377 .map(|(idx, key)| u8::try_from(idx).map(|idx| (map.get(key).unwrap(), idx)))
378 {
379 let (pubkey, idx) = res.map_err(|_| DecodeError::custom("invalid account keys"))?;
380 event_authority_indices
381 .entry(pubkey)
382 .or_default()
383 .insert(idx);
384 }
385 tracing::debug!("event_authorities: {event_authority_indices:#?}");
386 let Some(ixs) =
387 Option::<&Vec<_>>::from(self.transaction_status_meta.inner_instructions.as_ref())
388 else {
389 return Err(DecodeError::custom("missing inner instructions"));
390 };
391 let mut events = Vec::default();
392 for ix in ixs.iter().flat_map(|ixs| &ixs.instructions) {
393 let UiInstruction::Compiled(ix) = ix else {
394 tracing::warn!("only compiled instruction is currently supported");
395 continue;
396 };
397 if ix.accounts.len() != 1 {
399 continue;
400 }
401 if let Some(program_id) = accounts.get(ix.program_id_index as usize) {
402 let Some(indexes) = event_authority_indices.get(program_id) else {
403 continue;
404 };
405 let data = bs58::decode(&ix.data)
406 .into_vec()
407 .map_err(|err| {
408 DecodeError::custom(format!("decode ix data error, err={err}. Note that currently only Base58 is supported"))
409 })?;
410 if indexes.contains(&ix.accounts[0]) && data.starts_with(EVENT_IX_TAG_LE) {
411 events.push(CPIEvent::new(*program_id, data));
412 }
413 }
414 }
415 Ok(CPIEvents {
416 signature: self.signature,
417 slot_index: self.slot_index,
418 events,
419 })
420 }
421}
422
423impl crate::TransactionAccess for DecodedTransaction<'_> {
424 fn slot(&self) -> Result<u64, DecodeError> {
425 Ok(self.slot_index.0)
426 }
427
428 fn index(&self) -> Result<Option<usize>, DecodeError> {
429 Ok(self.slot_index.1)
430 }
431
432 fn signature(&self) -> Result<&Signature, DecodeError> {
433 Ok(&self.signature)
434 }
435
436 fn num_signers(&self, is_writable: bool) -> Result<usize, DecodeError> {
437 let header = self.transaction.message.header();
438 if is_writable {
439 (header.num_required_signatures as usize)
440 .checked_sub(self.num_signers(false)?)
441 .ok_or_else(|| {
442 DecodeError::custom(
443 "invalid transaction message header: num_signed < num_readonly_signed",
444 )
445 })
446 } else {
447 Ok(header.num_readonly_signed_accounts as usize)
448 }
449 }
450
451 fn num_accounts(&self) -> usize {
452 self.transaction.message.static_account_keys().len()
453 + self.dynamic_writable_accounts.len()
454 + self.dynamic_readonly_accounts.len()
455 }
456
457 fn message_signature(&self, idx: usize) -> Option<&Signature> {
458 self.transaction.signatures.get(idx)
459 }
460
461 fn account_meta(&self, idx: usize) -> Result<Option<AccountMeta>, DecodeError> {
462 let static_accounts = self.transaction.message.static_account_keys();
463 let static_end = static_accounts.len();
464 let dynamic_writable_length = self.dynamic_writable_accounts.len();
465 let dynamic_readonly_length = self.dynamic_readonly_accounts.len();
466 let dynamic_writable_end = static_end + dynamic_writable_length;
467 let dynamic_end = dynamic_writable_end + dynamic_readonly_length;
468 let meta = if idx >= dynamic_end {
469 None
470 } else if idx >= dynamic_writable_end {
471 let idx = idx - dynamic_writable_end;
472 Some(AccountMeta {
473 pubkey: self.dynamic_readonly_accounts[idx],
474 is_signer: false,
475 is_writable: false,
476 })
477 } else if idx >= static_end {
478 let idx = idx - static_end;
479 Some(AccountMeta {
480 pubkey: self.dynamic_writable_accounts[idx],
481 is_signer: false,
482 is_writable: true,
483 })
484 } else {
485 let num_readonly_signed = self.num_signers(false)?;
486 let num_readonly_unsigned = self
487 .transaction
488 .message
489 .header()
490 .num_readonly_unsigned_accounts as usize;
491 let writable_signed_end = self.num_signers(true)?;
492 let readonly_signed_end = writable_signed_end + num_readonly_signed;
493 let writable_unsigend_end = static_end.checked_sub(num_readonly_unsigned).ok_or_else(|| {
494 DecodeError::custom("invalid transaction message header: static_end < num_sigend + num_readonly_signed")
495 })?;
496 let (is_signer, is_writable) = if idx >= writable_unsigend_end {
497 (false, false)
498 } else if idx >= readonly_signed_end {
499 (false, true)
500 } else if idx >= writable_signed_end {
501 (true, false)
502 } else {
503 (true, true)
504 };
505 Some(AccountMeta {
506 pubkey: static_accounts[idx],
507 is_signer,
508 is_writable,
509 })
510 };
511 Ok(meta)
512 }
513
514 fn num_address_table_lookups(&self) -> usize {
515 self.transaction
516 .message
517 .address_table_lookups()
518 .map(|atls| atls.len())
519 .unwrap_or_default()
520 }
521
522 fn address_table_lookup(&self, idx: usize) -> Option<&MessageAddressTableLookup> {
523 self.transaction.message.address_table_lookups()?.get(idx)
524 }
525
526 fn num_instructions(&self) -> usize {
527 self.transaction.message.instructions().len()
528 }
529
530 fn instruction(&self, idx: usize) -> Option<&CompiledInstruction> {
531 self.transaction.message.instructions().get(idx)
532 }
533
534 fn transaction_status_meta(&self) -> Option<&UiTransactionStatusMeta> {
535 Some(self.transaction_status_meta)
536 }
537}