sim-cli 0.6.0

CLI tool for running and comparing Solana simulator backtests
use std::collections::{BTreeSet, HashMap};

use solana_transaction_status::{
    EncodedConfirmedTransactionWithStatusMeta, EncodedTransaction, UiMessage,
    option_serializer::OptionSerializer,
};

use crate::output::{SolChange, TokenChange, Transaction};

/// Build a partial [`Transaction`] from a Solana-standard
/// [`EncodedConfirmedTransactionWithStatusMeta`]. `logs` and `account_diffs`
/// are left empty for callers to fill in. Returns `None` if the meta or the
/// inner transaction body is missing/unusable.
///
/// The payload is produced by the server's `transactionSubscribe` stream,
/// which mirrors the shape of a standard `getTransaction` RPC response. There
/// is no fallback HTTP path: the CLI relies on subscriptions for *all*
/// transaction-level data.
pub fn transaction_from_encoded(
    confirmed: EncodedConfirmedTransactionWithStatusMeta,
    signature: &str,
    slot: u64,
) -> Option<Transaction> {
    let block_time = confirmed.block_time.map(|t| t as u64);
    let inner = confirmed.transaction;
    let meta = inner.meta.as_ref()?;
    let error = meta.err.as_ref().map(|e| e.to_string());

    let static_keys: Vec<String> = match &inner.transaction {
        EncodedTransaction::Json(ui_tx) => match &ui_tx.message {
            UiMessage::Raw(raw) => raw.account_keys.clone(),
            UiMessage::Parsed(parsed) => parsed
                .account_keys
                .iter()
                .map(|k| k.pubkey.clone())
                .collect(),
        },
        _ => return None,
    };

    let mut keys = static_keys;
    if let OptionSerializer::Some(addresses) = &meta.loaded_addresses {
        keys.extend(addresses.writable.iter().cloned());
        keys.extend(addresses.readonly.iter().cloned());
    }

    let sol_changes: Vec<SolChange> = keys
        .iter()
        .enumerate()
        .filter_map(|(i, pubkey)| {
            let pre = *meta.pre_balances.get(i)?;
            let post = *meta.post_balances.get(i)?;
            if pre == post {
                return None;
            }
            Some(SolChange {
                pubkey: pubkey.clone(),
                pre_lamports: pre,
                post_lamports: post,
            })
        })
        .collect();

    let empty = Vec::new();
    let pre_tokens = match &meta.pre_token_balances {
        OptionSerializer::Some(v) => v,
        _ => &empty,
    };
    let post_tokens = match &meta.post_token_balances {
        OptionSerializer::Some(v) => v,
        _ => &empty,
    };

    let mut pre_map: HashMap<u8, (u64, String, String, u8)> = HashMap::new();
    for t in pre_tokens {
        let amount = t.ui_token_amount.amount.parse::<u64>().unwrap_or(0);
        let owner = match &t.owner {
            OptionSerializer::Some(o) => o.clone(),
            _ => String::new(),
        };
        pre_map.insert(
            t.account_index,
            (amount, t.mint.clone(), owner, t.ui_token_amount.decimals),
        );
    }

    let mut post_map: HashMap<u8, (u64, String, String, u8)> = HashMap::new();
    for t in post_tokens {
        let amount = t.ui_token_amount.amount.parse::<u64>().unwrap_or(0);
        let owner = match &t.owner {
            OptionSerializer::Some(o) => o.clone(),
            _ => String::new(),
        };
        post_map.insert(
            t.account_index,
            (amount, t.mint.clone(), owner, t.ui_token_amount.decimals),
        );
    }

    let mut all_indices: BTreeSet<u8> = BTreeSet::new();
    all_indices.extend(pre_map.keys().copied());
    all_indices.extend(post_map.keys().copied());

    let token_changes: Vec<TokenChange> = all_indices
        .into_iter()
        .filter_map(|idx| {
            let pubkey = keys.get(idx as usize)?.clone();
            let (pre_amount, mint, owner, decimals) = if let Some(info) = pre_map.get(&idx) {
                (info.0, info.1.clone(), info.2.clone(), info.3)
            } else {
                let info = post_map.get(&idx)?;
                (0, info.1.clone(), info.2.clone(), info.3)
            };
            let post_amount = post_map.get(&idx).map(|i| i.0).unwrap_or(0);
            if pre_amount == post_amount {
                return None;
            }
            Some(TokenChange {
                pubkey,
                mint,
                owner,
                pre_amount,
                post_amount,
                decimals,
            })
        })
        .collect();

    Some(Transaction {
        slot,
        timestamp: block_time,
        signature: signature.to_string(),
        success: error.is_none(),
        error,
        sol_changes,
        token_changes,
        logs: Vec::new(),
        account_diffs: Vec::new(),
    })
}