1use {
4 crate::{
5 geyser_plugin_postgres::{GeyserPluginPostgresConfig, GeyserPluginPostgresError},
6 postgres_client::{DbWorkItem, ParallelPostgresClient, SimplePostgresClient},
7 },
8 chrono::Utc,
9 log::*,
10 postgres::{Client, Statement},
11 postgres_types::{FromSql, ToSql},
12 solana_geyser_plugin_interface::geyser_plugin_interface::{
13 GeyserPluginError, ReplicaTransactionInfo,
14 },
15 solana_runtime::bank::RewardType,
16 solana_sdk::{
17 instruction::CompiledInstruction,
18 message::{
19 v0::{self, LoadedAddresses, MessageAddressTableLookup},
20 Message, MessageHeader, SanitizedMessage,
21 },
22 transaction::TransactionError,
23 },
24 solana_transaction_status::{
25 InnerInstructions, Reward, TransactionStatusMeta, TransactionTokenBalance,
26 },
27};
28
29const MAX_TRANSACTION_STATUS_LEN: usize = 256;
30
31#[derive(Clone, Debug, FromSql, ToSql)]
32#[postgres(name = "CompiledInstruction")]
33pub struct DbCompiledInstruction {
34 pub program_id_index: i16,
35 pub accounts: Vec<i16>,
36 pub data: Vec<u8>,
37}
38
39#[derive(Clone, Debug, FromSql, ToSql)]
40#[postgres(name = "InnerInstructions")]
41pub struct DbInnerInstructions {
42 pub index: i16,
43 pub instructions: Vec<DbCompiledInstruction>,
44}
45
46#[derive(Clone, Debug, FromSql, ToSql)]
47#[postgres(name = "TransactionTokenBalance")]
48pub struct DbTransactionTokenBalance {
49 pub account_index: i16,
50 pub mint: String,
51 pub ui_token_amount: Option<f64>,
52 pub owner: String,
53}
54
55#[derive(Clone, Debug, FromSql, ToSql, PartialEq)]
56#[postgres(name = "RewardType")]
57pub enum DbRewardType {
58 Fee,
59 Rent,
60 Staking,
61 Voting,
62}
63
64#[derive(Clone, Debug, FromSql, ToSql)]
65#[postgres(name = "Reward")]
66pub struct DbReward {
67 pub pubkey: String,
68 pub lamports: i64,
69 pub post_balance: i64,
70 pub reward_type: Option<DbRewardType>,
71 pub commission: Option<i16>,
72}
73
74#[derive(Clone, Debug, FromSql, ToSql)]
75#[postgres(name = "TransactionStatusMeta")]
76pub struct DbTransactionStatusMeta {
77 pub error: Option<DbTransactionError>,
78 pub fee: i64,
79 pub pre_balances: Vec<i64>,
80 pub post_balances: Vec<i64>,
81 pub inner_instructions: Option<Vec<DbInnerInstructions>>,
82 pub log_messages: Option<Vec<String>>,
83 pub pre_token_balances: Option<Vec<DbTransactionTokenBalance>>,
84 pub post_token_balances: Option<Vec<DbTransactionTokenBalance>>,
85 pub rewards: Option<Vec<DbReward>>,
86}
87
88#[derive(Clone, Debug, FromSql, ToSql)]
89#[postgres(name = "TransactionMessageHeader")]
90pub struct DbTransactionMessageHeader {
91 pub num_required_signatures: i16,
92 pub num_readonly_signed_accounts: i16,
93 pub num_readonly_unsigned_accounts: i16,
94}
95
96#[derive(Clone, Debug, FromSql, ToSql)]
97#[postgres(name = "TransactionMessage")]
98pub struct DbTransactionMessage {
99 pub header: DbTransactionMessageHeader,
100 pub account_keys: Vec<Vec<u8>>,
101 pub recent_blockhash: Vec<u8>,
102 pub instructions: Vec<DbCompiledInstruction>,
103}
104
105#[derive(Clone, Debug, FromSql, ToSql)]
106#[postgres(name = "TransactionMessageAddressTableLookup")]
107pub struct DbTransactionMessageAddressTableLookup {
108 pub account_key: Vec<u8>,
109 pub writable_indexes: Vec<i16>,
110 pub readonly_indexes: Vec<i16>,
111}
112
113#[derive(Clone, Debug, FromSql, ToSql)]
114#[postgres(name = "TransactionMessageV0")]
115pub struct DbTransactionMessageV0 {
116 pub header: DbTransactionMessageHeader,
117 pub account_keys: Vec<Vec<u8>>,
118 pub recent_blockhash: Vec<u8>,
119 pub instructions: Vec<DbCompiledInstruction>,
120 pub address_table_lookups: Vec<DbTransactionMessageAddressTableLookup>,
121}
122
123#[derive(Clone, Debug, FromSql, ToSql)]
124#[postgres(name = "LoadedAddresses")]
125pub struct DbLoadedAddresses {
126 pub writable: Vec<Vec<u8>>,
127 pub readonly: Vec<Vec<u8>>,
128}
129
130#[derive(Clone, Debug, FromSql, ToSql)]
131#[postgres(name = "LoadedMessageV0")]
132pub struct DbLoadedMessageV0 {
133 pub message: DbTransactionMessageV0,
134 pub loaded_addresses: DbLoadedAddresses,
135}
136
137pub struct DbTransaction {
138 pub signature: Vec<u8>,
139 pub is_vote: bool,
140 pub slot: i64,
141 pub message_type: i16,
142 pub legacy_message: Option<DbTransactionMessage>,
143 pub v0_loaded_message: Option<DbLoadedMessageV0>,
144 pub message_hash: Vec<u8>,
145 pub meta: DbTransactionStatusMeta,
146 pub signatures: Vec<Vec<u8>>,
147}
148
149pub struct LogTransactionRequest {
150 pub transaction_info: DbTransaction,
151}
152
153impl From<&MessageAddressTableLookup> for DbTransactionMessageAddressTableLookup {
154 fn from(address_table_lookup: &MessageAddressTableLookup) -> Self {
155 Self {
156 account_key: address_table_lookup.account_key.as_ref().to_vec(),
157 writable_indexes: address_table_lookup
158 .writable_indexes
159 .iter()
160 .map(|idx| *idx as i16)
161 .collect(),
162 readonly_indexes: address_table_lookup
163 .readonly_indexes
164 .iter()
165 .map(|idx| *idx as i16)
166 .collect(),
167 }
168 }
169}
170
171impl From<&LoadedAddresses> for DbLoadedAddresses {
172 fn from(loaded_addresses: &LoadedAddresses) -> Self {
173 Self {
174 writable: loaded_addresses
175 .writable
176 .iter()
177 .map(|pubkey| pubkey.as_ref().to_vec())
178 .collect(),
179 readonly: loaded_addresses
180 .readonly
181 .iter()
182 .map(|pubkey| pubkey.as_ref().to_vec())
183 .collect(),
184 }
185 }
186}
187
188impl From<&MessageHeader> for DbTransactionMessageHeader {
189 fn from(header: &MessageHeader) -> Self {
190 Self {
191 num_required_signatures: header.num_required_signatures as i16,
192 num_readonly_signed_accounts: header.num_readonly_signed_accounts as i16,
193 num_readonly_unsigned_accounts: header.num_readonly_unsigned_accounts as i16,
194 }
195 }
196}
197
198impl From<&CompiledInstruction> for DbCompiledInstruction {
199 fn from(instruction: &CompiledInstruction) -> Self {
200 Self {
201 program_id_index: instruction.program_id_index as i16,
202 accounts: instruction
203 .accounts
204 .iter()
205 .map(|account_idx| *account_idx as i16)
206 .collect(),
207 data: instruction.data.clone(),
208 }
209 }
210}
211
212impl From<&Message> for DbTransactionMessage {
213 fn from(message: &Message) -> Self {
214 Self {
215 header: DbTransactionMessageHeader::from(&message.header),
216 account_keys: message
217 .account_keys
218 .iter()
219 .map(|key| key.as_ref().to_vec())
220 .collect(),
221 recent_blockhash: message.recent_blockhash.as_ref().to_vec(),
222 instructions: message
223 .instructions
224 .iter()
225 .map(DbCompiledInstruction::from)
226 .collect(),
227 }
228 }
229}
230
231impl From<&v0::Message> for DbTransactionMessageV0 {
232 fn from(message: &v0::Message) -> Self {
233 Self {
234 header: DbTransactionMessageHeader::from(&message.header),
235 account_keys: message
236 .account_keys
237 .iter()
238 .map(|key| key.as_ref().to_vec())
239 .collect(),
240 recent_blockhash: message.recent_blockhash.as_ref().to_vec(),
241 instructions: message
242 .instructions
243 .iter()
244 .map(DbCompiledInstruction::from)
245 .collect(),
246 address_table_lookups: message
247 .address_table_lookups
248 .iter()
249 .map(DbTransactionMessageAddressTableLookup::from)
250 .collect(),
251 }
252 }
253}
254
255impl<'a> From<&v0::LoadedMessage<'a>> for DbLoadedMessageV0 {
256 fn from(message: &v0::LoadedMessage) -> Self {
257 Self {
258 message: DbTransactionMessageV0::from(&message.message as &v0::Message),
259 loaded_addresses: DbLoadedAddresses::from(
260 &message.loaded_addresses as &LoadedAddresses,
261 ),
262 }
263 }
264}
265
266impl From<&InnerInstructions> for DbInnerInstructions {
267 fn from(instructions: &InnerInstructions) -> Self {
268 Self {
269 index: instructions.index as i16,
270 instructions: instructions
271 .instructions
272 .iter()
273 .map(DbCompiledInstruction::from)
274 .collect(),
275 }
276 }
277}
278
279impl From<&RewardType> for DbRewardType {
280 fn from(reward_type: &RewardType) -> Self {
281 match reward_type {
282 RewardType::Fee => Self::Fee,
283 RewardType::Rent => Self::Rent,
284 RewardType::Staking => Self::Staking,
285 RewardType::Voting => Self::Voting,
286 }
287 }
288}
289
290fn get_reward_type(reward: &Option<RewardType>) -> Option<DbRewardType> {
291 reward.as_ref().map(DbRewardType::from)
292}
293
294impl From<&Reward> for DbReward {
295 fn from(reward: &Reward) -> Self {
296 Self {
297 pubkey: reward.pubkey.clone(),
298 lamports: reward.lamports as i64,
299 post_balance: reward.post_balance as i64,
300 reward_type: get_reward_type(&reward.reward_type),
301 commission: reward
302 .commission
303 .as_ref()
304 .map(|commission| *commission as i16),
305 }
306 }
307}
308
309#[derive(Clone, Debug, FromSql, ToSql, PartialEq)]
310#[postgres(name = "TransactionErrorCode")]
311pub enum DbTransactionErrorCode {
312 AccountInUse,
313 AccountLoadedTwice,
314 AccountNotFound,
315 ProgramAccountNotFound,
316 InsufficientFundsForFee,
317 InvalidAccountForFee,
318 AlreadyProcessed,
319 BlockhashNotFound,
320 InstructionError,
321 CallChainTooDeep,
322 MissingSignatureForFee,
323 InvalidAccountIndex,
324 SignatureFailure,
325 InvalidProgramForExecution,
326 SanitizeFailure,
327 ClusterMaintenance,
328 AccountBorrowOutstanding,
329 WouldExceedMaxAccountCostLimit,
330 WouldExceedMaxBlockCostLimit,
331 UnsupportedVersion,
332 InvalidWritableAccount,
333 WouldExceedMaxAccountDataCostLimit,
334 TooManyAccountLocks,
335 AddressLookupTableNotFound,
336 InvalidAddressLookupTableOwner,
337 InvalidAddressLookupTableData,
338 InvalidAddressLookupTableIndex,
339 InvalidRentPayingAccount,
340 WouldExceedMaxVoteCostLimit,
341 WouldExceedAccountDataBlockLimit,
342 WouldExceedAccountDataTotalLimit,
343}
344
345impl From<&TransactionError> for DbTransactionErrorCode {
346 fn from(err: &TransactionError) -> Self {
347 match err {
348 TransactionError::AccountInUse => Self::AccountInUse,
349 TransactionError::AccountLoadedTwice => Self::AccountLoadedTwice,
350 TransactionError::AccountNotFound => Self::AccountNotFound,
351 TransactionError::ProgramAccountNotFound => Self::ProgramAccountNotFound,
352 TransactionError::InsufficientFundsForFee => Self::InsufficientFundsForFee,
353 TransactionError::InvalidAccountForFee => Self::InvalidAccountForFee,
354 TransactionError::AlreadyProcessed => Self::AlreadyProcessed,
355 TransactionError::BlockhashNotFound => Self::BlockhashNotFound,
356 TransactionError::InstructionError(_idx, _error) => Self::InstructionError,
357 TransactionError::CallChainTooDeep => Self::CallChainTooDeep,
358 TransactionError::MissingSignatureForFee => Self::MissingSignatureForFee,
359 TransactionError::InvalidAccountIndex => Self::InvalidAccountIndex,
360 TransactionError::SignatureFailure => Self::SignatureFailure,
361 TransactionError::InvalidProgramForExecution => Self::InvalidProgramForExecution,
362 TransactionError::SanitizeFailure => Self::SanitizeFailure,
363 TransactionError::ClusterMaintenance => Self::ClusterMaintenance,
364 TransactionError::AccountBorrowOutstanding => Self::AccountBorrowOutstanding,
365 TransactionError::WouldExceedMaxAccountCostLimit => {
366 Self::WouldExceedMaxAccountCostLimit
367 }
368 TransactionError::WouldExceedMaxBlockCostLimit => Self::WouldExceedMaxBlockCostLimit,
369 TransactionError::UnsupportedVersion => Self::UnsupportedVersion,
370 TransactionError::InvalidWritableAccount => Self::InvalidWritableAccount,
371 TransactionError::WouldExceedAccountDataBlockLimit => {
372 Self::WouldExceedAccountDataBlockLimit
373 }
374 TransactionError::WouldExceedAccountDataTotalLimit => {
375 Self::WouldExceedAccountDataTotalLimit
376 }
377 TransactionError::TooManyAccountLocks => Self::TooManyAccountLocks,
378 TransactionError::AddressLookupTableNotFound => Self::AddressLookupTableNotFound,
379 TransactionError::InvalidAddressLookupTableOwner => {
380 Self::InvalidAddressLookupTableOwner
381 }
382 TransactionError::InvalidAddressLookupTableData => Self::InvalidAddressLookupTableData,
383 TransactionError::InvalidAddressLookupTableIndex => {
384 Self::InvalidAddressLookupTableIndex
385 }
386 TransactionError::InvalidRentPayingAccount => Self::InvalidRentPayingAccount,
387 TransactionError::WouldExceedMaxVoteCostLimit => Self::WouldExceedMaxVoteCostLimit,
388 }
389 }
390}
391
392#[derive(Clone, Debug, FromSql, ToSql, PartialEq)]
393#[postgres(name = "TransactionError")]
394pub struct DbTransactionError {
395 error_code: DbTransactionErrorCode,
396 error_detail: Option<String>,
397}
398
399fn get_transaction_error(result: &Result<(), TransactionError>) -> Option<DbTransactionError> {
400 if result.is_ok() {
401 return None;
402 }
403
404 let error = result.as_ref().err().unwrap();
405 Some(DbTransactionError {
406 error_code: DbTransactionErrorCode::from(error),
407 error_detail: {
408 if let TransactionError::InstructionError(idx, instruction_error) = error {
409 let mut error_detail = format!(
410 "InstructionError: idx ({}), error: ({})",
411 idx, instruction_error
412 );
413 if error_detail.len() > MAX_TRANSACTION_STATUS_LEN {
414 error_detail = error_detail
415 .to_string()
416 .split_off(MAX_TRANSACTION_STATUS_LEN);
417 }
418 Some(error_detail)
419 } else {
420 None
421 }
422 },
423 })
424}
425
426impl From<&TransactionTokenBalance> for DbTransactionTokenBalance {
427 fn from(token_balance: &TransactionTokenBalance) -> Self {
428 Self {
429 account_index: token_balance.account_index as i16,
430 mint: token_balance.mint.clone(),
431 ui_token_amount: token_balance.ui_token_amount.ui_amount,
432 owner: token_balance.owner.clone(),
433 }
434 }
435}
436
437impl From<&TransactionStatusMeta> for DbTransactionStatusMeta {
438 fn from(meta: &TransactionStatusMeta) -> Self {
439 Self {
440 error: get_transaction_error(&meta.status),
441 fee: meta.fee as i64,
442 pre_balances: meta
443 .pre_balances
444 .iter()
445 .map(|balance| *balance as i64)
446 .collect(),
447 post_balances: meta
448 .post_balances
449 .iter()
450 .map(|balance| *balance as i64)
451 .collect(),
452 inner_instructions: meta
453 .inner_instructions
454 .as_ref()
455 .map(|instructions| instructions.iter().map(DbInnerInstructions::from).collect()),
456 log_messages: meta.log_messages.clone(),
457 pre_token_balances: meta.pre_token_balances.as_ref().map(|balances| {
458 balances
459 .iter()
460 .map(DbTransactionTokenBalance::from)
461 .collect()
462 }),
463 post_token_balances: meta.post_token_balances.as_ref().map(|balances| {
464 balances
465 .iter()
466 .map(DbTransactionTokenBalance::from)
467 .collect()
468 }),
469 rewards: meta
470 .rewards
471 .as_ref()
472 .map(|rewards| rewards.iter().map(DbReward::from).collect()),
473 }
474 }
475}
476
477fn build_db_transaction(slot: u64, transaction_info: &ReplicaTransactionInfo) -> DbTransaction {
478 DbTransaction {
479 signature: transaction_info.signature.as_ref().to_vec(),
480 is_vote: transaction_info.is_vote,
481 slot: slot as i64,
482 message_type: match transaction_info.transaction.message() {
483 SanitizedMessage::Legacy(_) => 0,
484 SanitizedMessage::V0(_) => 1,
485 },
486 legacy_message: match transaction_info.transaction.message() {
487 SanitizedMessage::Legacy(legacy_message) => {
488 Some(DbTransactionMessage::from(legacy_message))
489 }
490 _ => None,
491 },
492 v0_loaded_message: match transaction_info.transaction.message() {
493 SanitizedMessage::V0(loaded_message) => Some(DbLoadedMessageV0::from(loaded_message)),
494 _ => None,
495 },
496 signatures: transaction_info
497 .transaction
498 .signatures()
499 .iter()
500 .map(|signature| signature.as_ref().to_vec())
501 .collect(),
502 message_hash: transaction_info
503 .transaction
504 .message_hash()
505 .as_ref()
506 .to_vec(),
507 meta: DbTransactionStatusMeta::from(transaction_info.transaction_status_meta),
508 }
509}
510
511impl SimplePostgresClient {
512 pub(crate) fn build_transaction_info_upsert_statement(
513 client: &mut Client,
514 config: &GeyserPluginPostgresConfig,
515 ) -> Result<Statement, GeyserPluginError> {
516 let stmt = "INSERT INTO transaction AS txn (signature, is_vote, slot, message_type, legacy_message, \
517 v0_loaded_message, signatures, message_hash, meta, updated_on) \
518 VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) \
519 ON CONFLICT (slot, signature) DO UPDATE SET is_vote=excluded.is_vote, \
520 message_type=excluded.message_type, \
521 legacy_message=excluded.legacy_message, \
522 v0_loaded_message=excluded.v0_loaded_message, \
523 signatures=excluded.signatures, \
524 message_hash=excluded.message_hash, \
525 meta=excluded.meta, \
526 updated_on=excluded.updated_on";
527
528 let stmt = client.prepare(stmt);
529
530 match stmt {
531 Err(err) => {
532 return Err(GeyserPluginError::Custom(Box::new(GeyserPluginPostgresError::DataSchemaError {
533 msg: format!(
534 "Error in preparing for the transaction update PostgreSQL database: ({}) host: {:?} user: {:?} config: {:?}",
535 err, config.host, config.user, config
536 ),
537 })));
538 }
539 Ok(stmt) => Ok(stmt),
540 }
541 }
542
543 pub(crate) fn log_transaction_impl(
544 &mut self,
545 transaction_log_info: LogTransactionRequest,
546 ) -> Result<(), GeyserPluginError> {
547 let client = self.client.get_mut().unwrap();
548 let statement = &client.update_transaction_log_stmt;
549 let client = &mut client.client;
550 let updated_on = Utc::now().naive_utc();
551
552 let transaction_info = transaction_log_info.transaction_info;
553 let result = client.query(
554 statement,
555 &[
556 &transaction_info.signature,
557 &transaction_info.is_vote,
558 &transaction_info.slot,
559 &transaction_info.message_type,
560 &transaction_info.legacy_message,
561 &transaction_info.v0_loaded_message,
562 &transaction_info.signatures,
563 &transaction_info.message_hash,
564 &transaction_info.meta,
565 &updated_on,
566 ],
567 );
568
569 if let Err(err) = result {
570 let msg = format!(
571 "Failed to persist the update of transaction info to the PostgreSQL database. Error: {:?}",
572 err
573 );
574 error!("{}", msg);
575 return Err(GeyserPluginError::AccountsUpdateError { msg });
576 }
577
578 Ok(())
579 }
580}
581
582impl ParallelPostgresClient {
583 fn build_transaction_request(
584 slot: u64,
585 transaction_info: &ReplicaTransactionInfo,
586 ) -> LogTransactionRequest {
587 LogTransactionRequest {
588 transaction_info: build_db_transaction(slot, transaction_info),
589 }
590 }
591
592 pub fn log_transaction_info(
593 &mut self,
594 transaction_info: &ReplicaTransactionInfo,
595 slot: u64,
596 ) -> Result<(), GeyserPluginError> {
597 let wrk_item = DbWorkItem::LogTransaction(Box::new(Self::build_transaction_request(
598 slot,
599 transaction_info,
600 )));
601
602 if let Err(err) = self.sender.send(wrk_item) {
603 return Err(GeyserPluginError::SlotStatusUpdateError {
604 msg: format!("Failed to update the transaction, error: {:?}", err),
605 });
606 }
607 Ok(())
608 }
609}
610
611#[cfg(test)]
612pub(crate) mod tests {
613 use {
614 super::*,
615 solana_account_decoder::parse_token::UiTokenAmount,
616 solana_sdk::{
617 hash::Hash,
618 message::VersionedMessage,
619 pubkey::Pubkey,
620 sanitize::Sanitize,
621 signature::{Keypair, Signature, Signer},
622 system_transaction,
623 transaction::{
624 SanitizedTransaction, SimpleAddressLoader, Transaction, VersionedTransaction,
625 },
626 },
627 };
628
629 fn check_compiled_instruction_equality(
630 compiled_instruction: &CompiledInstruction,
631 db_compiled_instruction: &DbCompiledInstruction,
632 ) {
633 assert_eq!(
634 compiled_instruction.program_id_index,
635 db_compiled_instruction.program_id_index as u8
636 );
637 assert_eq!(
638 compiled_instruction.accounts.len(),
639 db_compiled_instruction.accounts.len()
640 );
641 assert_eq!(
642 compiled_instruction.data.len(),
643 db_compiled_instruction.data.len()
644 );
645
646 for i in 0..compiled_instruction.accounts.len() {
647 assert_eq!(
648 compiled_instruction.accounts[i],
649 db_compiled_instruction.accounts[i] as u8
650 )
651 }
652 for i in 0..compiled_instruction.data.len() {
653 assert_eq!(
654 compiled_instruction.data[i],
655 db_compiled_instruction.data[i] as u8
656 )
657 }
658 }
659
660 #[test]
661 fn test_transform_compiled_instruction() {
662 let compiled_instruction = CompiledInstruction {
663 program_id_index: 0,
664 accounts: vec![1, 2, 3],
665 data: vec![4, 5, 6],
666 };
667
668 let db_compiled_instruction = DbCompiledInstruction::from(&compiled_instruction);
669 check_compiled_instruction_equality(&compiled_instruction, &db_compiled_instruction);
670 }
671
672 fn check_inner_instructions_equality(
673 inner_instructions: &InnerInstructions,
674 db_inner_instructions: &DbInnerInstructions,
675 ) {
676 assert_eq!(inner_instructions.index, db_inner_instructions.index as u8);
677 assert_eq!(
678 inner_instructions.instructions.len(),
679 db_inner_instructions.instructions.len()
680 );
681
682 for i in 0..inner_instructions.instructions.len() {
683 check_compiled_instruction_equality(
684 &inner_instructions.instructions[i],
685 &db_inner_instructions.instructions[i],
686 )
687 }
688 }
689
690 #[test]
691 fn test_transform_inner_instructions() {
692 let inner_instructions = InnerInstructions {
693 index: 0,
694 instructions: vec![
695 CompiledInstruction {
696 program_id_index: 0,
697 accounts: vec![1, 2, 3],
698 data: vec![4, 5, 6],
699 },
700 CompiledInstruction {
701 program_id_index: 1,
702 accounts: vec![12, 13, 14],
703 data: vec![24, 25, 26],
704 },
705 ],
706 };
707
708 let db_inner_instructions = DbInnerInstructions::from(&inner_instructions);
709 check_inner_instructions_equality(&inner_instructions, &db_inner_instructions);
710 }
711
712 fn check_address_table_lookups_equality(
713 address_table_lookups: &MessageAddressTableLookup,
714 db_address_table_lookups: &DbTransactionMessageAddressTableLookup,
715 ) {
716 assert_eq!(
717 address_table_lookups.writable_indexes.len(),
718 db_address_table_lookups.writable_indexes.len()
719 );
720 assert_eq!(
721 address_table_lookups.readonly_indexes.len(),
722 db_address_table_lookups.readonly_indexes.len()
723 );
724
725 for i in 0..address_table_lookups.writable_indexes.len() {
726 assert_eq!(
727 address_table_lookups.writable_indexes[i],
728 db_address_table_lookups.writable_indexes[i] as u8
729 )
730 }
731 for i in 0..address_table_lookups.readonly_indexes.len() {
732 assert_eq!(
733 address_table_lookups.readonly_indexes[i],
734 db_address_table_lookups.readonly_indexes[i] as u8
735 )
736 }
737 }
738
739 #[test]
740 fn test_transform_address_table_lookups() {
741 let address_table_lookups = MessageAddressTableLookup {
742 account_key: Pubkey::new_unique(),
743 writable_indexes: vec![1, 2, 3],
744 readonly_indexes: vec![4, 5, 6],
745 };
746
747 let db_address_table_lookups =
748 DbTransactionMessageAddressTableLookup::from(&address_table_lookups);
749 check_address_table_lookups_equality(&address_table_lookups, &db_address_table_lookups);
750 }
751
752 fn check_reward_equality(reward: &Reward, db_reward: &DbReward) {
753 assert_eq!(reward.pubkey, db_reward.pubkey);
754 assert_eq!(reward.lamports, db_reward.lamports);
755 assert_eq!(reward.post_balance, db_reward.post_balance as u64);
756 assert_eq!(get_reward_type(&reward.reward_type), db_reward.reward_type);
757 assert_eq!(
758 reward.commission,
759 db_reward
760 .commission
761 .as_ref()
762 .map(|commission| *commission as u8)
763 );
764 }
765
766 #[test]
767 fn test_transform_reward() {
768 let reward = Reward {
769 pubkey: Pubkey::new_unique().to_string(),
770 lamports: 1234,
771 post_balance: 45678,
772 reward_type: Some(RewardType::Fee),
773 commission: Some(12),
774 };
775
776 let db_reward = DbReward::from(&reward);
777 check_reward_equality(&reward, &db_reward);
778 }
779
780 fn check_transaction_token_balance_equality(
781 transaction_token_balance: &TransactionTokenBalance,
782 db_transaction_token_balance: &DbTransactionTokenBalance,
783 ) {
784 assert_eq!(
785 transaction_token_balance.account_index,
786 db_transaction_token_balance.account_index as u8
787 );
788 assert_eq!(
789 transaction_token_balance.mint,
790 db_transaction_token_balance.mint
791 );
792 assert_eq!(
793 transaction_token_balance.ui_token_amount.ui_amount,
794 db_transaction_token_balance.ui_token_amount
795 );
796 assert_eq!(
797 transaction_token_balance.owner,
798 db_transaction_token_balance.owner
799 );
800 }
801
802 #[test]
803 fn test_transform_transaction_token_balance() {
804 let transaction_token_balance = TransactionTokenBalance {
805 account_index: 3,
806 mint: Pubkey::new_unique().to_string(),
807 ui_token_amount: UiTokenAmount {
808 ui_amount: Some(0.42),
809 decimals: 2,
810 amount: "42".to_string(),
811 ui_amount_string: "0.42".to_string(),
812 },
813 owner: Pubkey::new_unique().to_string(),
814 };
815
816 let db_transaction_token_balance =
817 DbTransactionTokenBalance::from(&transaction_token_balance);
818
819 check_transaction_token_balance_equality(
820 &transaction_token_balance,
821 &db_transaction_token_balance,
822 );
823 }
824
825 fn check_token_balances(
826 token_balances: &Option<Vec<TransactionTokenBalance>>,
827 db_token_balances: &Option<Vec<DbTransactionTokenBalance>>,
828 ) {
829 assert_eq!(
830 token_balances
831 .as_ref()
832 .map(|token_balances| token_balances.len()),
833 db_token_balances
834 .as_ref()
835 .map(|token_balances| token_balances.len()),
836 );
837
838 if token_balances.is_some() {
839 for i in 0..token_balances.as_ref().unwrap().len() {
840 check_transaction_token_balance_equality(
841 &token_balances.as_ref().unwrap()[i],
842 &db_token_balances.as_ref().unwrap()[i],
843 );
844 }
845 }
846 }
847
848 fn check_transaction_status_meta(
849 transaction_status_meta: &TransactionStatusMeta,
850 db_transaction_status_meta: &DbTransactionStatusMeta,
851 ) {
852 assert_eq!(
853 get_transaction_error(&transaction_status_meta.status),
854 db_transaction_status_meta.error
855 );
856 assert_eq!(
857 transaction_status_meta.fee,
858 db_transaction_status_meta.fee as u64
859 );
860 assert_eq!(
861 transaction_status_meta.pre_balances.len(),
862 db_transaction_status_meta.pre_balances.len()
863 );
864
865 for i in 0..transaction_status_meta.pre_balances.len() {
866 assert_eq!(
867 transaction_status_meta.pre_balances[i],
868 db_transaction_status_meta.pre_balances[i] as u64
869 );
870 }
871 assert_eq!(
872 transaction_status_meta.post_balances.len(),
873 db_transaction_status_meta.post_balances.len()
874 );
875 for i in 0..transaction_status_meta.post_balances.len() {
876 assert_eq!(
877 transaction_status_meta.post_balances[i],
878 db_transaction_status_meta.post_balances[i] as u64
879 );
880 }
881 assert_eq!(
882 transaction_status_meta
883 .inner_instructions
884 .as_ref()
885 .map(|inner_instructions| inner_instructions.len()),
886 db_transaction_status_meta
887 .inner_instructions
888 .as_ref()
889 .map(|inner_instructions| inner_instructions.len()),
890 );
891
892 if transaction_status_meta.inner_instructions.is_some() {
893 for i in 0..transaction_status_meta
894 .inner_instructions
895 .as_ref()
896 .unwrap()
897 .len()
898 {
899 check_inner_instructions_equality(
900 &transaction_status_meta.inner_instructions.as_ref().unwrap()[i],
901 &db_transaction_status_meta
902 .inner_instructions
903 .as_ref()
904 .unwrap()[i],
905 );
906 }
907 }
908
909 assert_eq!(
910 transaction_status_meta
911 .log_messages
912 .as_ref()
913 .map(|log_messages| log_messages.len()),
914 db_transaction_status_meta
915 .log_messages
916 .as_ref()
917 .map(|log_messages| log_messages.len()),
918 );
919
920 if transaction_status_meta.log_messages.is_some() {
921 for i in 0..transaction_status_meta.log_messages.as_ref().unwrap().len() {
922 assert_eq!(
923 &transaction_status_meta.log_messages.as_ref().unwrap()[i],
924 &db_transaction_status_meta.log_messages.as_ref().unwrap()[i]
925 );
926 }
927 }
928
929 check_token_balances(
930 &transaction_status_meta.pre_token_balances,
931 &db_transaction_status_meta.pre_token_balances,
932 );
933
934 check_token_balances(
935 &transaction_status_meta.post_token_balances,
936 &db_transaction_status_meta.post_token_balances,
937 );
938
939 assert_eq!(
940 transaction_status_meta
941 .rewards
942 .as_ref()
943 .map(|rewards| rewards.len()),
944 db_transaction_status_meta
945 .rewards
946 .as_ref()
947 .map(|rewards| rewards.len()),
948 );
949
950 if transaction_status_meta.rewards.is_some() {
951 for i in 0..transaction_status_meta.rewards.as_ref().unwrap().len() {
952 check_reward_equality(
953 &transaction_status_meta.rewards.as_ref().unwrap()[i],
954 &db_transaction_status_meta.rewards.as_ref().unwrap()[i],
955 );
956 }
957 }
958 }
959
960 fn build_transaction_status_meta() -> TransactionStatusMeta {
961 TransactionStatusMeta {
962 status: Ok(()),
963 fee: 23456,
964 pre_balances: vec![11, 22, 33],
965 post_balances: vec![44, 55, 66],
966 inner_instructions: Some(vec![InnerInstructions {
967 index: 0,
968 instructions: vec![
969 CompiledInstruction {
970 program_id_index: 0,
971 accounts: vec![1, 2, 3],
972 data: vec![4, 5, 6],
973 },
974 CompiledInstruction {
975 program_id_index: 1,
976 accounts: vec![12, 13, 14],
977 data: vec![24, 25, 26],
978 },
979 ],
980 }]),
981 log_messages: Some(vec!["message1".to_string(), "message2".to_string()]),
982 pre_token_balances: Some(vec![
983 TransactionTokenBalance {
984 account_index: 3,
985 mint: Pubkey::new_unique().to_string(),
986 ui_token_amount: UiTokenAmount {
987 ui_amount: Some(0.42),
988 decimals: 2,
989 amount: "42".to_string(),
990 ui_amount_string: "0.42".to_string(),
991 },
992 owner: Pubkey::new_unique().to_string(),
993 },
994 TransactionTokenBalance {
995 account_index: 2,
996 mint: Pubkey::new_unique().to_string(),
997 ui_token_amount: UiTokenAmount {
998 ui_amount: Some(0.38),
999 decimals: 2,
1000 amount: "38".to_string(),
1001 ui_amount_string: "0.38".to_string(),
1002 },
1003 owner: Pubkey::new_unique().to_string(),
1004 },
1005 ]),
1006 post_token_balances: Some(vec![
1007 TransactionTokenBalance {
1008 account_index: 3,
1009 mint: Pubkey::new_unique().to_string(),
1010 ui_token_amount: UiTokenAmount {
1011 ui_amount: Some(0.82),
1012 decimals: 2,
1013 amount: "82".to_string(),
1014 ui_amount_string: "0.82".to_string(),
1015 },
1016 owner: Pubkey::new_unique().to_string(),
1017 },
1018 TransactionTokenBalance {
1019 account_index: 2,
1020 mint: Pubkey::new_unique().to_string(),
1021 ui_token_amount: UiTokenAmount {
1022 ui_amount: Some(0.48),
1023 decimals: 2,
1024 amount: "48".to_string(),
1025 ui_amount_string: "0.48".to_string(),
1026 },
1027 owner: Pubkey::new_unique().to_string(),
1028 },
1029 ]),
1030 rewards: Some(vec![
1031 Reward {
1032 pubkey: Pubkey::new_unique().to_string(),
1033 lamports: 1234,
1034 post_balance: 45678,
1035 reward_type: Some(RewardType::Fee),
1036 commission: Some(12),
1037 },
1038 Reward {
1039 pubkey: Pubkey::new_unique().to_string(),
1040 lamports: 234,
1041 post_balance: 324,
1042 reward_type: Some(RewardType::Staking),
1043 commission: Some(11),
1044 },
1045 ]),
1046 loaded_addresses: LoadedAddresses {
1047 writable: vec![Pubkey::new_unique(), Pubkey::new_unique()],
1048 readonly: vec![Pubkey::new_unique(), Pubkey::new_unique()],
1049 },
1050 }
1051 }
1052
1053 #[test]
1054 fn test_transform_transaction_status_meta() {
1055 let transaction_status_meta = build_transaction_status_meta();
1056 let db_transaction_status_meta = DbTransactionStatusMeta::from(&transaction_status_meta);
1057 check_transaction_status_meta(&transaction_status_meta, &db_transaction_status_meta);
1058 }
1059
1060 fn check_message_header_equality(
1061 message_header: &MessageHeader,
1062 db_message_header: &DbTransactionMessageHeader,
1063 ) {
1064 assert_eq!(
1065 message_header.num_readonly_signed_accounts,
1066 db_message_header.num_readonly_signed_accounts as u8
1067 );
1068 assert_eq!(
1069 message_header.num_readonly_unsigned_accounts,
1070 db_message_header.num_readonly_unsigned_accounts as u8
1071 );
1072 assert_eq!(
1073 message_header.num_required_signatures,
1074 db_message_header.num_required_signatures as u8
1075 );
1076 }
1077
1078 #[test]
1079 fn test_transform_transaction_message_header() {
1080 let message_header = MessageHeader {
1081 num_readonly_signed_accounts: 1,
1082 num_readonly_unsigned_accounts: 2,
1083 num_required_signatures: 3,
1084 };
1085
1086 let db_message_header = DbTransactionMessageHeader::from(&message_header);
1087 check_message_header_equality(&message_header, &db_message_header)
1088 }
1089
1090 fn check_transaction_message_equality(message: &Message, db_message: &DbTransactionMessage) {
1091 check_message_header_equality(&message.header, &db_message.header);
1092 assert_eq!(message.account_keys.len(), db_message.account_keys.len());
1093 for i in 0..message.account_keys.len() {
1094 assert_eq!(message.account_keys[i].as_ref(), db_message.account_keys[i]);
1095 }
1096 assert_eq!(message.instructions.len(), db_message.instructions.len());
1097 for i in 0..message.instructions.len() {
1098 check_compiled_instruction_equality(
1099 &message.instructions[i],
1100 &db_message.instructions[i],
1101 );
1102 }
1103 }
1104
1105 fn build_message() -> Message {
1106 Message {
1107 header: MessageHeader {
1108 num_readonly_signed_accounts: 11,
1109 num_readonly_unsigned_accounts: 12,
1110 num_required_signatures: 13,
1111 },
1112 account_keys: vec![Pubkey::new_unique(), Pubkey::new_unique()],
1113 recent_blockhash: Hash::new_unique(),
1114 instructions: vec![
1115 CompiledInstruction {
1116 program_id_index: 0,
1117 accounts: vec![1, 2, 3],
1118 data: vec![4, 5, 6],
1119 },
1120 CompiledInstruction {
1121 program_id_index: 3,
1122 accounts: vec![11, 12, 13],
1123 data: vec![14, 15, 16],
1124 },
1125 ],
1126 }
1127 }
1128
1129 #[test]
1130 fn test_transform_transaction_message() {
1131 let message = build_message();
1132
1133 let db_message = DbTransactionMessage::from(&message);
1134 check_transaction_message_equality(&message, &db_message);
1135 }
1136
1137 fn check_transaction_message_v0_equality(
1138 message: &v0::Message,
1139 db_message: &DbTransactionMessageV0,
1140 ) {
1141 check_message_header_equality(&message.header, &db_message.header);
1142 assert_eq!(message.account_keys.len(), db_message.account_keys.len());
1143 for i in 0..message.account_keys.len() {
1144 assert_eq!(message.account_keys[i].as_ref(), db_message.account_keys[i]);
1145 }
1146 assert_eq!(message.instructions.len(), db_message.instructions.len());
1147 for i in 0..message.instructions.len() {
1148 check_compiled_instruction_equality(
1149 &message.instructions[i],
1150 &db_message.instructions[i],
1151 );
1152 }
1153 assert_eq!(
1154 message.address_table_lookups.len(),
1155 db_message.address_table_lookups.len()
1156 );
1157 for i in 0..message.address_table_lookups.len() {
1158 check_address_table_lookups_equality(
1159 &message.address_table_lookups[i],
1160 &db_message.address_table_lookups[i],
1161 );
1162 }
1163 }
1164
1165 fn build_transaction_message_v0() -> v0::Message {
1166 v0::Message {
1167 header: MessageHeader {
1168 num_readonly_signed_accounts: 2,
1169 num_readonly_unsigned_accounts: 2,
1170 num_required_signatures: 3,
1171 },
1172 account_keys: vec![
1173 Pubkey::new_unique(),
1174 Pubkey::new_unique(),
1175 Pubkey::new_unique(),
1176 Pubkey::new_unique(),
1177 Pubkey::new_unique(),
1178 ],
1179 recent_blockhash: Hash::new_unique(),
1180 instructions: vec![
1181 CompiledInstruction {
1182 program_id_index: 1,
1183 accounts: vec![1, 2, 3],
1184 data: vec![4, 5, 6],
1185 },
1186 CompiledInstruction {
1187 program_id_index: 2,
1188 accounts: vec![0, 1, 2],
1189 data: vec![14, 15, 16],
1190 },
1191 ],
1192 address_table_lookups: vec![
1193 MessageAddressTableLookup {
1194 account_key: Pubkey::new_unique(),
1195 writable_indexes: vec![0],
1196 readonly_indexes: vec![1, 2],
1197 },
1198 MessageAddressTableLookup {
1199 account_key: Pubkey::new_unique(),
1200 writable_indexes: vec![1],
1201 readonly_indexes: vec![0, 2],
1202 },
1203 ],
1204 }
1205 }
1206
1207 #[test]
1208 fn test_transform_transaction_message_v0() {
1209 let message = build_transaction_message_v0();
1210
1211 let db_message = DbTransactionMessageV0::from(&message);
1212 check_transaction_message_v0_equality(&message, &db_message);
1213 }
1214
1215 fn check_loaded_addresses(
1216 loaded_addresses: &LoadedAddresses,
1217 db_loaded_addresses: &DbLoadedAddresses,
1218 ) {
1219 assert_eq!(
1220 loaded_addresses.writable.len(),
1221 db_loaded_addresses.writable.len()
1222 );
1223 for i in 0..loaded_addresses.writable.len() {
1224 assert_eq!(
1225 loaded_addresses.writable[i].as_ref(),
1226 db_loaded_addresses.writable[i]
1227 );
1228 }
1229
1230 assert_eq!(
1231 loaded_addresses.readonly.len(),
1232 db_loaded_addresses.readonly.len()
1233 );
1234 for i in 0..loaded_addresses.readonly.len() {
1235 assert_eq!(
1236 loaded_addresses.readonly[i].as_ref(),
1237 db_loaded_addresses.readonly[i]
1238 );
1239 }
1240 }
1241
1242 fn check_loaded_message_v0_equality(
1243 message: &v0::LoadedMessage,
1244 db_message: &DbLoadedMessageV0,
1245 ) {
1246 check_transaction_message_v0_equality(&message.message, &db_message.message);
1247 check_loaded_addresses(&message.loaded_addresses, &db_message.loaded_addresses);
1248 }
1249
1250 #[test]
1251 fn test_transform_loaded_message_v0() {
1252 let message = v0::LoadedMessage::new(
1253 build_transaction_message_v0(),
1254 LoadedAddresses {
1255 writable: vec![Pubkey::new_unique(), Pubkey::new_unique()],
1256 readonly: vec![Pubkey::new_unique(), Pubkey::new_unique()],
1257 },
1258 );
1259
1260 let db_message = DbLoadedMessageV0::from(&message);
1261 check_loaded_message_v0_equality(&message, &db_message);
1262 }
1263
1264 fn check_transaction(
1265 slot: u64,
1266 transaction: &ReplicaTransactionInfo,
1267 db_transaction: &DbTransaction,
1268 ) {
1269 assert_eq!(transaction.signature.as_ref(), db_transaction.signature);
1270 assert_eq!(transaction.is_vote, db_transaction.is_vote);
1271 assert_eq!(slot, db_transaction.slot as u64);
1272 match transaction.transaction.message() {
1273 SanitizedMessage::Legacy(message) => {
1274 assert_eq!(db_transaction.message_type, 0);
1275 check_transaction_message_equality(
1276 message,
1277 db_transaction.legacy_message.as_ref().unwrap(),
1278 );
1279 }
1280 SanitizedMessage::V0(message) => {
1281 assert_eq!(db_transaction.message_type, 1);
1282 check_loaded_message_v0_equality(
1283 message,
1284 db_transaction.v0_loaded_message.as_ref().unwrap(),
1285 );
1286 }
1287 }
1288
1289 assert_eq!(
1290 transaction.transaction.signatures().len(),
1291 db_transaction.signatures.len()
1292 );
1293
1294 for i in 0..transaction.transaction.signatures().len() {
1295 assert_eq!(
1296 transaction.transaction.signatures()[i].as_ref(),
1297 db_transaction.signatures[i]
1298 );
1299 }
1300
1301 assert_eq!(
1302 transaction.transaction.message_hash().as_ref(),
1303 db_transaction.message_hash
1304 );
1305
1306 check_transaction_status_meta(transaction.transaction_status_meta, &db_transaction.meta);
1307 }
1308
1309 fn build_test_transaction_legacy() -> Transaction {
1310 let keypair1 = Keypair::new();
1311 let pubkey1 = keypair1.pubkey();
1312 let zero = Hash::default();
1313 system_transaction::transfer(&keypair1, &pubkey1, 42, zero)
1314 }
1315
1316 #[test]
1317 fn test_build_db_transaction_legacy() {
1318 let signature = Signature::new(&[1u8; 64]);
1319
1320 let message_hash = Hash::new_unique();
1321 let transaction = build_test_transaction_legacy();
1322
1323 let transaction = VersionedTransaction::from(transaction);
1324
1325 let transaction = SanitizedTransaction::try_create(
1326 transaction,
1327 message_hash,
1328 Some(true),
1329 SimpleAddressLoader::Disabled,
1330 )
1331 .unwrap();
1332
1333 let transaction_status_meta = build_transaction_status_meta();
1334 let transaction_info = ReplicaTransactionInfo {
1335 signature: &signature,
1336 is_vote: false,
1337 transaction: &transaction,
1338 transaction_status_meta: &transaction_status_meta,
1339 };
1340
1341 let slot = 54;
1342 let db_transaction = build_db_transaction(slot, &transaction_info);
1343 check_transaction(slot, &transaction_info, &db_transaction);
1344 }
1345
1346 fn build_test_transaction_v0() -> VersionedTransaction {
1347 VersionedTransaction {
1348 signatures: vec![
1349 Signature::new(&[1u8; 64]),
1350 Signature::new(&[2u8; 64]),
1351 Signature::new(&[3u8; 64]),
1352 ],
1353 message: VersionedMessage::V0(build_transaction_message_v0()),
1354 }
1355 }
1356
1357 #[test]
1358 fn test_build_db_transaction_v0() {
1359 let signature = Signature::new(&[1u8; 64]);
1360
1361 let message_hash = Hash::new_unique();
1362 let transaction = build_test_transaction_v0();
1363
1364 transaction.sanitize().unwrap();
1365
1366 let transaction = SanitizedTransaction::try_create(
1367 transaction,
1368 message_hash,
1369 Some(true),
1370 SimpleAddressLoader::Enabled(LoadedAddresses {
1371 writable: vec![Pubkey::new_unique(), Pubkey::new_unique()],
1372 readonly: vec![Pubkey::new_unique(), Pubkey::new_unique()],
1373 }),
1374 )
1375 .unwrap();
1376
1377 let transaction_status_meta = build_transaction_status_meta();
1378 let transaction_info = ReplicaTransactionInfo {
1379 signature: &signature,
1380 is_vote: true,
1381 transaction: &transaction,
1382 transaction_status_meta: &transaction_status_meta,
1383 };
1384
1385 let slot = 54;
1386 let db_transaction = build_db_transaction(slot, &transaction_info);
1387 check_transaction(slot, &transaction_info, &db_transaction);
1388 }
1389}