Skip to main content

replay_core/
lib.rs

1//! # replay-core
2//!
3//! The engine for Replay — a time-travel debugger for Solana transactions.
4//!
5//! The public surface is intentionally small:
6//! - [`replay`] — one-shot replay of a mainnet transaction.
7//! - [`fork`] — create a session you can mutate and re-execute.
8//!
9//! Everything else is a private implementation detail. If you find yourself
10//! reaching for internals from outside this crate, that's a signal to widen
11//! the public API here rather than bypass it.
12
13pub mod error;
14pub mod types;
15pub mod rpc;
16pub mod fetch;
17pub mod reconstruct;
18pub mod svm;
19pub mod idl;
20pub mod trace;
21pub mod session;
22
23#[cfg(test)]
24pub(crate) mod test_support;
25
26pub use error::ReplayError;
27pub use types::{
28    AccountDelta, AccountMutation, CpiFrame, FetchedTx, FrameAccount, LogDivergence,
29    ProgramInfo, ProgramLoader, ReconstructedState, Trace, TraceDiff, TxContext, TxResult,
30};
31pub use rpc::{HeliusClient, HeliusRpcClient};
32pub use session::ForkedSession;
33
34use solana_sdk::pubkey::Pubkey;
35use solana_sdk::signature::Signature;
36use std::str::FromStr;
37
38/// Replay a mainnet transaction against litesvm with historical state.
39///
40/// Equivalent to [`fork`] followed by [`ForkedSession::execute`], but without
41/// retaining the session. Use this when you just want to inspect what
42/// happened; use [`fork`] when you want to mutate state and ask what-if.
43pub async fn replay<C: HeliusClient>(
44    signature: &str,
45    client: &C,
46) -> Result<Trace, ReplayError> {
47    let sig = Signature::from_str(signature)
48        .map_err(|_| ReplayError::InvalidSignature(signature.to_string()))?;
49
50    let mut ctx = fetch::fetch_full_tx_context(client, &sig).await?;
51    let state = reconstruct::reconstruct_state(client, &ctx).await?;
52    ctx.pre_account_snapshots = reconstruct::snapshot_pre_state(&state);
53
54    let mut runner = svm::SvmRunner::new();
55    runner.seed(&state)?;
56    runner.set_clock_for_slot(ctx.slot, ctx.block_time);
57
58    let execution = runner.execute(&ctx)?;
59
60    let idl_cache = idl::IdlCache::default();
61    let decoder = idl::AccountDecoder::new(&idl_cache);
62
63    let mut trace = trace::build_trace(&ctx, &execution, &decoder).await;
64    decode_account_deltas(&mut trace, &ctx, &execution, &decoder, client).await;
65
66    Ok(trace)
67}
68
69/// Walk `trace.account_deltas` and populate `decoded_before` / `decoded_after`
70/// (and `idl_type_name` when available) by running the decoder against each
71/// pre/post snapshot. Idempotent and side-effect-free outside the trace.
72pub async fn decode_account_deltas<C: HeliusClient>(
73    trace: &mut Trace,
74    ctx: &TxContext,
75    execution: &svm::ExecutionResult,
76    decoder: &idl::AccountDecoder<'_>,
77    client: &C,
78) {
79    for delta in &mut trace.account_deltas {
80        let Ok(pubkey) = Pubkey::from_str(&delta.pubkey) else {
81            continue;
82        };
83        if let Some(before) = ctx.pre_account_snapshots.get(&pubkey) {
84            let dec = decoder.decode(&pubkey, before, client).await;
85            if let idl::DecodedAccount::Decoded { type_name, .. }
86            | idl::DecodedAccount::Native { type_name, .. } = &dec
87            {
88                delta.idl_type_name.get_or_insert_with(|| type_name.clone());
89            }
90            delta.decoded_before = serde_json::to_value(&dec).ok();
91        }
92        if let Some(after) = execution.accounts_after.get(&pubkey) {
93            let dec = decoder.decode(&pubkey, after, client).await;
94            if let idl::DecodedAccount::Decoded { type_name, .. }
95            | idl::DecodedAccount::Native { type_name, .. } = &dec
96            {
97                delta.idl_type_name.get_or_insert_with(|| type_name.clone());
98            }
99            delta.decoded_after = serde_json::to_value(&dec).ok();
100        }
101    }
102}
103
104/// Create a forked session seeded from a mainnet transaction.
105///
106/// The session holds all reconstructed state in-memory. Mutations apply
107/// on top; [`ForkedSession::execute`] re-runs the transaction against the
108/// mutated state. A baseline trace is captured at fork time so you can
109/// diff any subsequent run against the original outcome.
110pub async fn fork<C: HeliusClient>(
111    signature: &str,
112    client: &C,
113) -> Result<ForkedSession, ReplayError> {
114    let sig = Signature::from_str(signature)
115        .map_err(|_| ReplayError::InvalidSignature(signature.to_string()))?;
116
117    let mut ctx = fetch::fetch_full_tx_context(client, &sig).await?;
118    let state = reconstruct::reconstruct_state(client, &ctx).await?;
119    ctx.pre_account_snapshots = reconstruct::snapshot_pre_state(&state);
120
121    ForkedSession::new(ctx, state).await
122}