solana_accountsdb_plugin_postgres/postgres_client/
postgres_client_transaction.rs

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