1use std::fmt;
2
3use serde_json::Value;
4use solana_sdk::{
5 clock::{Slot, UnixTimestamp},
6 instruction::CompiledInstruction,
7 message::{
8 v0::{self, LoadedAddresses, LoadedMessage, MessageAddressTableLookup},
9 AccountKeys, Message, MessageHeader, VersionedMessage,
10 },
11 transaction::{
12 Result as TransactionResult, TransactionError, TransactionVersion, VersionedTransaction,
13 },
14 transaction_context::TransactionReturnData,
15};
16use thiserror::Error;
17
18use crate::{account_decoder::parse_token::UiTokenAmount, runtime::RewardType};
19
20#[derive(Serialize, Deserialize, Clone, Copy, Debug, Eq, Hash, PartialEq)]
21#[serde(rename_all = "camelCase")]
22pub enum TransactionBinaryEncoding {
23 Base58,
24 Base64,
25}
26
27#[derive(Serialize, Deserialize, Clone, Copy, Debug, Eq, Hash, PartialEq)]
28#[serde(rename_all = "camelCase")]
29pub enum UiTransactionEncoding {
30 Binary, Base64,
32 Base58,
33 Json,
34 JsonParsed,
35}
36
37impl UiTransactionEncoding {
38 pub fn into_binary_encoding(&self) -> Option<TransactionBinaryEncoding> {
39 match self {
40 Self::Binary | Self::Base58 => Some(TransactionBinaryEncoding::Base58),
41 Self::Base64 => Some(TransactionBinaryEncoding::Base64),
42 _ => None,
43 }
44 }
45}
46
47impl fmt::Display for UiTransactionEncoding {
48 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
49 let v = serde_json::to_value(self).map_err(|_| fmt::Error)?;
50 let s = v.as_str().ok_or(fmt::Error)?;
51 write!(f, "{}", s)
52 }
53}
54
55#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
56#[serde(rename_all = "camelCase")]
57pub struct ParsedAccount {
58 pub pubkey: String,
59 pub writable: bool,
60 pub signer: bool,
61}
62
63#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
65#[serde(rename_all = "camelCase")]
66pub struct UiTransaction {
67 pub signatures: Vec<String>,
68 pub message: UiMessage,
69}
70
71#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
73#[serde(rename_all = "camelCase")]
74pub struct UiCompiledInstruction {
75 pub program_id_index: u8,
76 pub accounts: Vec<u8>,
77 pub data: String,
78}
79
80impl From<&CompiledInstruction> for UiCompiledInstruction {
81 fn from(instruction: &CompiledInstruction) -> Self {
82 Self {
83 program_id_index: instruction.program_id_index,
84 accounts: instruction.accounts.clone(),
85 data: bs58::encode(instruction.data.clone()).into_string(),
86 }
87 }
88}
89
90#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
91#[serde(rename_all = "camelCase")]
92pub struct ParsedInstruction {
93 pub program: String,
94 pub program_id: String,
95 pub parsed: Value,
96}
97
98#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
100#[serde(rename_all = "camelCase")]
101pub struct UiPartiallyDecodedInstruction {
102 pub program_id: String,
103 pub accounts: Vec<String>,
104 pub data: String,
105}
106
107impl UiPartiallyDecodedInstruction {
108 fn from(instruction: &CompiledInstruction, account_keys: &AccountKeys) -> Self {
109 Self {
110 program_id: account_keys[instruction.program_id_index as usize].to_string(),
111 accounts: instruction
112 .accounts
113 .iter()
114 .map(|&i| account_keys[i as usize].to_string())
115 .collect(),
116 data: bs58::encode(instruction.data.clone()).into_string(),
117 }
118 }
119}
120
121#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
122#[serde(rename_all = "camelCase", untagged)]
123pub enum UiParsedInstruction {
124 Parsed(ParsedInstruction),
125 PartiallyDecoded(UiPartiallyDecodedInstruction),
126}
127
128#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
129#[serde(rename_all = "camelCase")]
130pub enum ParsableProgram {
131 SplAssociatedTokenAccount,
132 SplMemo,
133 SplToken,
134 BpfLoader,
135 BpfUpgradeableLoader,
136 Stake,
137 System,
138 Vote,
139}
140
141#[derive(Error, Debug)]
142pub enum ParseInstructionError {
143 #[error("{0:?} instruction not parsable")]
144 InstructionNotParsable(ParsableProgram),
145
146 #[error("{0:?} instruction key mismatch")]
147 InstructionKeyMismatch(ParsableProgram),
148
149 #[error("Program not parsable")]
150 ProgramNotParsable,
151
152 #[error("Internal error, please report")]
153 SerdeJsonError(#[from] serde_json::error::Error),
154}
155
156#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
222#[serde(rename_all = "camelCase", untagged)]
223pub enum UiInstruction {
224 Compiled(UiCompiledInstruction),
225 Parsed(UiParsedInstruction),
226}
227
228impl UiInstruction {
229 fn parse(instruction: &CompiledInstruction, account_keys: &AccountKeys) -> Self {
230 UiInstruction::Parsed(UiParsedInstruction::PartiallyDecoded(
241 UiPartiallyDecodedInstruction::from(instruction, account_keys),
242 ))
243 }
244}
245
246#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
248#[serde(rename_all = "camelCase")]
249pub struct UiAddressTableLookup {
250 pub account_key: String,
251 pub writable_indexes: Vec<u8>,
252 pub readonly_indexes: Vec<u8>,
253}
254
255impl From<&MessageAddressTableLookup> for UiAddressTableLookup {
256 fn from(lookup: &MessageAddressTableLookup) -> Self {
257 Self {
258 account_key: lookup.account_key.to_string(),
259 writable_indexes: lookup.writable_indexes.clone(),
260 readonly_indexes: lookup.readonly_indexes.clone(),
261 }
262 }
263}
264
265#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
267#[serde(rename_all = "camelCase")]
268pub struct UiParsedMessage {
269 pub account_keys: Vec<ParsedAccount>,
270 pub recent_blockhash: String,
271 pub instructions: Vec<UiInstruction>,
272 pub address_table_lookups: Option<Vec<UiAddressTableLookup>>,
273}
274
275#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
277#[serde(rename_all = "camelCase")]
278pub struct UiRawMessage {
279 pub header: MessageHeader,
280 pub account_keys: Vec<String>,
281 pub recent_blockhash: String,
282 pub instructions: Vec<UiCompiledInstruction>,
283 #[serde(default, skip_serializing_if = "Option::is_none")]
284 pub address_table_lookups: Option<Vec<UiAddressTableLookup>>,
285}
286
287#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
288#[serde(rename_all = "camelCase", untagged)]
289pub enum UiMessage {
290 Parsed(UiParsedMessage),
291 Raw(UiRawMessage),
292}
293
294#[derive(Clone, Debug, PartialEq)]
295pub struct TransactionStatusMeta {
296 pub status: TransactionResult<()>,
297 pub fee: u64,
298 pub pre_balances: Vec<u64>,
299 pub post_balances: Vec<u64>,
300 pub inner_instructions: Option<Vec<InnerInstructions>>,
301 pub log_messages: Option<Vec<String>>,
302 pub pre_token_balances: Option<Vec<TransactionTokenBalance>>,
303 pub post_token_balances: Option<Vec<TransactionTokenBalance>>,
304 pub rewards: Option<Rewards>,
305 pub loaded_addresses: LoadedAddresses,
306 pub return_data: Option<TransactionReturnData>,
307}
308
309impl Default for TransactionStatusMeta {
310 fn default() -> Self {
311 Self {
312 status: Ok(()),
313 fee: 0,
314 pre_balances: vec![],
315 post_balances: vec![],
316 inner_instructions: None,
317 log_messages: None,
318 pre_token_balances: None,
319 post_token_balances: None,
320 rewards: None,
321 loaded_addresses: LoadedAddresses::default(),
322 return_data: None,
323 }
324 }
325}
326
327pub fn parse_accounts(message: &Message) -> Vec<ParsedAccount> {
328 let mut accounts: Vec<ParsedAccount> = vec![];
329 for (i, account_key) in message.account_keys.iter().enumerate() {
330 accounts.push(ParsedAccount {
331 pubkey: account_key.to_string(),
332 writable: message.is_writable(i),
333 signer: message.is_signer(i),
334 });
335 }
336 accounts
337}
338
339pub fn parse_static_accounts(message: &LoadedMessage) -> Vec<ParsedAccount> {
340 let mut accounts: Vec<ParsedAccount> = vec![];
341 for (i, account_key) in message.static_account_keys().iter().enumerate() {
342 accounts.push(ParsedAccount {
343 pubkey: account_key.to_string(),
344 writable: message.is_writable(i),
345 signer: message.is_signer(i),
346 });
347 }
348 accounts
349}
350
351pub trait Encodable {
353 type Encoded;
354 fn encode(&self, encoding: UiTransactionEncoding) -> Self::Encoded;
355}
356
357impl Encodable for Message {
358 type Encoded = UiMessage;
359 fn encode(&self, encoding: UiTransactionEncoding) -> Self::Encoded {
360 if encoding == UiTransactionEncoding::JsonParsed {
361 let account_keys = AccountKeys::new(&self.account_keys, None);
362 UiMessage::Parsed(UiParsedMessage {
363 account_keys: parse_accounts(self),
364 recent_blockhash: self.recent_blockhash.to_string(),
365 instructions: self
366 .instructions
367 .iter()
368 .map(|instruction| UiInstruction::parse(instruction, &account_keys))
369 .collect(),
370 address_table_lookups: None,
371 })
372 } else {
373 UiMessage::Raw(UiRawMessage {
374 header: self.header,
375 account_keys: self.account_keys.iter().map(ToString::to_string).collect(),
376 recent_blockhash: self.recent_blockhash.to_string(),
377 instructions: self.instructions.iter().map(Into::into).collect(),
378 address_table_lookups: None,
379 })
380 }
381 }
382}
383
384impl EncodableWithMeta for v0::Message {
385 type Encoded = UiMessage;
386 fn encode_with_meta(
387 &self,
388 encoding: UiTransactionEncoding,
389 meta: &TransactionStatusMeta,
390 ) -> Self::Encoded {
391 if encoding == UiTransactionEncoding::JsonParsed {
392 let account_keys = AccountKeys::new(&self.account_keys, Some(&meta.loaded_addresses));
393 let loaded_message = LoadedMessage::new_borrowed(self, &meta.loaded_addresses);
394 UiMessage::Parsed(UiParsedMessage {
395 account_keys: parse_static_accounts(&loaded_message),
396 recent_blockhash: self.recent_blockhash.to_string(),
397 instructions: self
398 .instructions
399 .iter()
400 .map(|instruction| UiInstruction::parse(instruction, &account_keys))
401 .collect(),
402 address_table_lookups: Some(
403 self.address_table_lookups.iter().map(Into::into).collect(),
404 ),
405 })
406 } else {
407 self.json_encode()
408 }
409 }
410 fn json_encode(&self) -> Self::Encoded {
411 UiMessage::Raw(UiRawMessage {
412 header: self.header,
413 account_keys: self.account_keys.iter().map(ToString::to_string).collect(),
414 recent_blockhash: self.recent_blockhash.to_string(),
415 instructions: self.instructions.iter().map(Into::into).collect(),
416 address_table_lookups: Some(
417 self.address_table_lookups.iter().map(Into::into).collect(),
418 ),
419 })
420 }
421}
422
423pub trait EncodableWithMeta {
425 type Encoded;
426 fn encode_with_meta(
427 &self,
428 encoding: UiTransactionEncoding,
429 meta: &TransactionStatusMeta,
430 ) -> Self::Encoded;
431 fn json_encode(&self) -> Self::Encoded;
432}
433
434impl EncodableWithMeta for VersionedTransaction {
435 type Encoded = EncodedTransaction;
436 fn encode_with_meta(
437 &self,
438 encoding: UiTransactionEncoding,
439 meta: &TransactionStatusMeta,
440 ) -> Self::Encoded {
441 match encoding {
442 UiTransactionEncoding::Binary => EncodedTransaction::LegacyBinary(
443 bs58::encode(bincode::serialize(self).unwrap()).into_string(),
444 ),
445 UiTransactionEncoding::Base58 => EncodedTransaction::Binary(
446 bs58::encode(bincode::serialize(self).unwrap()).into_string(),
447 TransactionBinaryEncoding::Base58,
448 ),
449 UiTransactionEncoding::Base64 => EncodedTransaction::Binary(
450 base64::encode(bincode::serialize(self).unwrap()),
451 TransactionBinaryEncoding::Base64,
452 ),
453 UiTransactionEncoding::Json => self.json_encode(),
454 UiTransactionEncoding::JsonParsed => EncodedTransaction::Json(UiTransaction {
455 signatures: self.signatures.iter().map(ToString::to_string).collect(),
456 message: match &self.message {
457 VersionedMessage::Legacy(message) => {
458 message.encode(UiTransactionEncoding::JsonParsed)
459 }
460 VersionedMessage::V0(message) => {
461 message.encode_with_meta(UiTransactionEncoding::JsonParsed, meta)
462 }
463 },
464 }),
465 }
466 }
467 fn json_encode(&self) -> Self::Encoded {
468 EncodedTransaction::Json(UiTransaction {
469 signatures: self.signatures.iter().map(ToString::to_string).collect(),
470 message: match &self.message {
471 VersionedMessage::Legacy(message) => message.encode(UiTransactionEncoding::Json),
472 VersionedMessage::V0(message) => message.json_encode(),
473 },
474 })
475 }
476}
477
478#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
479#[serde(rename_all = "camelCase", untagged)]
480pub enum EncodedTransaction {
481 LegacyBinary(String), Binary(String, TransactionBinaryEncoding),
483 Json(UiTransaction),
484}
485
486impl EncodedTransaction {
487 pub fn decode(&self) -> Option<VersionedTransaction> {
488 let (blob, encoding) = match self {
489 Self::Json(_) => return None,
490 Self::LegacyBinary(blob) => (blob, TransactionBinaryEncoding::Base58),
491 Self::Binary(blob, encoding) => (blob, *encoding),
492 };
493
494 let transaction: Option<VersionedTransaction> = match encoding {
495 TransactionBinaryEncoding::Base58 => bs58::decode(blob)
496 .into_vec()
497 .ok()
498 .and_then(|bytes| bincode::deserialize(&bytes).ok()),
499 TransactionBinaryEncoding::Base64 => base64::decode(blob)
500 .ok()
501 .and_then(|bytes| bincode::deserialize(&bytes).ok()),
502 };
503
504 transaction.filter(|transaction| transaction.sanitize().is_ok())
505 }
506}
507
508#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
509#[serde(rename_all = "camelCase")]
510pub struct Reward {
511 pub pubkey: String,
512 pub lamports: i64,
513 pub post_balance: u64, pub reward_type: Option<RewardType>,
515 pub commission: Option<u8>, }
517
518pub type Rewards = Vec<Reward>;
519
520#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
521pub struct InnerInstructions {
522 pub index: u8,
524 pub instructions: Vec<CompiledInstruction>,
526}
527
528#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
529#[serde(rename_all = "camelCase")]
530pub struct UiInnerInstructions {
531 pub index: u8,
533 pub instructions: Vec<UiInstruction>,
535}
536
537impl UiInnerInstructions {
538 fn parse(inner_instructions: InnerInstructions, account_keys: &AccountKeys) -> Self {
539 Self {
540 index: inner_instructions.index,
541 instructions: inner_instructions
542 .instructions
543 .iter()
544 .map(|ix| UiInstruction::parse(ix, account_keys))
545 .collect(),
546 }
547 }
548}
549
550impl From<InnerInstructions> for UiInnerInstructions {
551 fn from(inner_instructions: InnerInstructions) -> Self {
552 Self {
553 index: inner_instructions.index,
554 instructions: inner_instructions
555 .instructions
556 .iter()
557 .map(|ix| UiInstruction::Compiled(ix.into()))
558 .collect(),
559 }
560 }
561}
562
563#[derive(Clone, Debug, PartialEq)]
564pub struct TransactionTokenBalance {
565 pub account_index: u8,
566 pub mint: String,
567 pub ui_token_amount: UiTokenAmount,
568 pub owner: String,
569 pub program_id: String,
570}
571
572#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
573#[serde(rename_all = "camelCase")]
574pub struct UiTransactionTokenBalance {
575 pub account_index: u8,
576 pub mint: String,
577 pub ui_token_amount: UiTokenAmount,
578 #[serde(default, skip_serializing_if = "Option::is_none")]
579 pub owner: Option<String>,
580 #[serde(default, skip_serializing_if = "Option::is_none")]
581 pub program_id: Option<String>,
582}
583
584impl From<TransactionTokenBalance> for UiTransactionTokenBalance {
585 fn from(token_balance: TransactionTokenBalance) -> Self {
586 Self {
587 account_index: token_balance.account_index,
588 mint: token_balance.mint,
589 ui_token_amount: token_balance.ui_token_amount,
590 owner: if !token_balance.owner.is_empty() {
591 Some(token_balance.owner)
592 } else {
593 None
594 },
595 program_id: if !token_balance.program_id.is_empty() {
596 Some(token_balance.program_id)
597 } else {
598 None
599 },
600 }
601 }
602}
603
604#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
606#[serde(rename_all = "camelCase")]
607pub struct UiLoadedAddresses {
608 pub writable: Vec<String>,
609 pub readonly: Vec<String>,
610}
611
612impl From<&LoadedAddresses> for UiLoadedAddresses {
613 fn from(loaded_addresses: &LoadedAddresses) -> Self {
614 Self {
615 writable: loaded_addresses
616 .writable
617 .iter()
618 .map(ToString::to_string)
619 .collect(),
620 readonly: loaded_addresses
621 .readonly
622 .iter()
623 .map(ToString::to_string)
624 .collect(),
625 }
626 }
627}
628
629#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
631#[serde(rename_all = "camelCase")]
632pub struct UiTransactionStatusMeta {
633 pub err: Option<TransactionError>,
634 pub status: TransactionResult<()>, pub fee: u64,
636 pub pre_balances: Vec<u64>,
637 pub post_balances: Vec<u64>,
638 pub inner_instructions: Option<Vec<UiInnerInstructions>>,
639 pub log_messages: Option<Vec<String>>,
640 pub pre_token_balances: Option<Vec<UiTransactionTokenBalance>>,
641 pub post_token_balances: Option<Vec<UiTransactionTokenBalance>>,
642 pub rewards: Option<Rewards>,
643 #[serde(default, skip_serializing_if = "Option::is_none")]
644 pub loaded_addresses: Option<UiLoadedAddresses>,
645 pub return_data: Option<TransactionReturnData>,
646}
647
648#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
649#[serde(rename_all = "camelCase")]
650pub struct EncodedTransactionWithStatusMeta {
651 pub transaction: EncodedTransaction,
652 pub meta: Option<UiTransactionStatusMeta>,
653 #[serde(default, skip_serializing_if = "Option::is_none")]
654 pub version: Option<TransactionVersion>,
655}
656
657#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
658#[serde(rename_all = "camelCase")]
659pub struct EncodedConfirmedTransactionWithStatusMeta {
660 pub slot: Slot,
661 #[serde(flatten)]
662 pub transaction: EncodedTransactionWithStatusMeta,
663 pub block_time: Option<UnixTimestamp>,
664}
665
666#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
667#[serde(rename_all = "camelCase")]
668pub enum TransactionConfirmationStatus {
669 Processed,
670 Confirmed,
671 Finalized,
672}
673
674#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq, Serialize, Deserialize)]
675#[serde(rename_all = "camelCase")]
676pub enum TransactionDetails {
677 Full,
678 Signatures,
679 None,
680}
681
682impl Default for TransactionDetails {
683 fn default() -> Self {
684 Self::Full
685 }
686}
687
688#[derive(Debug, PartialEq, Serialize, Deserialize)]
689#[serde(rename_all = "camelCase")]
690pub struct EncodedConfirmedBlock {
691 pub previous_blockhash: String,
692 pub blockhash: String,
693 pub parent_slot: Slot,
694 pub transactions: Vec<EncodedTransactionWithStatusMeta>,
695 pub rewards: Rewards,
696 pub block_time: Option<UnixTimestamp>,
697 pub block_height: Option<u64>,
698}
699
700impl From<UiConfirmedBlock> for EncodedConfirmedBlock {
701 fn from(block: UiConfirmedBlock) -> Self {
702 Self {
703 previous_blockhash: block.previous_blockhash,
704 blockhash: block.blockhash,
705 parent_slot: block.parent_slot,
706 transactions: block.transactions.unwrap_or_default(),
707 rewards: block.rewards.unwrap_or_default(),
708 block_time: block.block_time,
709 block_height: block.block_height,
710 }
711 }
712}
713
714#[derive(Debug, PartialEq, Serialize, Deserialize)]
715#[serde(rename_all = "camelCase")]
716pub struct UiConfirmedBlock {
717 pub previous_blockhash: String,
718 pub blockhash: String,
719 pub parent_slot: Slot,
720 #[serde(default, skip_serializing_if = "Option::is_none")]
721 pub transactions: Option<Vec<EncodedTransactionWithStatusMeta>>,
722 #[serde(default, skip_serializing_if = "Option::is_none")]
723 pub signatures: Option<Vec<String>>,
724 #[serde(default, skip_serializing_if = "Option::is_none")]
725 pub rewards: Option<Rewards>,
726 pub block_time: Option<UnixTimestamp>,
727 pub block_height: Option<u64>,
728}