1use crate::error::ReplayError;
8use crate::idl::{AccountDecoder, IdlCache};
9use crate::rpc::HeliusClient;
10use crate::svm::replay_from_scratch;
11use crate::trace::build_trace;
12use crate::types::{AccountMutation, ReconstructedState, Trace, TraceDiff, TxContext};
13use solana_sdk::{account::Account, pubkey::Pubkey};
14use std::collections::HashMap;
15
16pub struct ForkedSession {
17 pub id: String,
18 pub ctx: TxContext,
19 pub state: ReconstructedState,
20 pub baseline_trace: Trace,
21 pub mutations: Vec<(Pubkey, AccountMutation)>,
22 pub latest_trace: Option<Trace>,
23 pub created_at: std::time::Instant,
24}
25
26impl ForkedSession {
27 pub async fn new(
30 ctx: TxContext,
31 state: ReconstructedState,
32 ) -> Result<Self, ReplayError> {
33 let id = ulid::Ulid::new().to_string();
34
35 let execution = replay_from_scratch(&state, &ctx, &[])?;
37 let idl_cache = IdlCache::default();
38 let decoder = AccountDecoder::new(&idl_cache);
39 let baseline_trace = build_trace(&ctx, &execution, &decoder).await;
40
41 Ok(Self {
42 id,
43 ctx,
44 state,
45 baseline_trace,
46 mutations: Vec::new(),
47 latest_trace: None,
48 created_at: std::time::Instant::now(),
49 })
50 }
51
52 pub fn mutate(
54 &mut self,
55 pubkey: Pubkey,
56 mutation: AccountMutation,
57 ) -> Result<(), ReplayError> {
58 self.mutations.push((pubkey, mutation));
62 Ok(())
63 }
64
65 pub fn reset(&mut self) {
67 self.mutations.clear();
68 self.latest_trace = None;
69 }
70
71 pub async fn execute<C: HeliusClient>(
74 &mut self,
75 client: &C,
76 ) -> Result<Trace, ReplayError> {
77 let resolved = self.resolve_mutations(client).await?;
78 let execution = replay_from_scratch(&self.state, &self.ctx, &resolved)?;
79
80 let idl_cache = IdlCache::default();
81 let decoder = AccountDecoder::new(&idl_cache);
82 let trace = build_trace(&self.ctx, &execution, &decoder).await;
83
84 self.latest_trace = Some(trace.clone());
85 Ok(trace)
86 }
87
88 pub fn diff(&self) -> Option<TraceDiff> {
90 let latest = self.latest_trace.clone()?;
91 let result_changed = self.baseline_trace.replay_result != latest.replay_result;
92
93 let mut changed_accounts: Vec<String> = Vec::new();
94 let baseline_deltas: HashMap<_, _> = self
95 .baseline_trace
96 .account_deltas
97 .iter()
98 .map(|d| (d.pubkey.clone(), d))
99 .collect();
100 let latest_deltas: HashMap<_, _> = latest
101 .account_deltas
102 .iter()
103 .map(|d| (d.pubkey.clone(), d))
104 .collect();
105 for key in latest_deltas.keys() {
106 match baseline_deltas.get(key) {
107 None => changed_accounts.push(key.clone()),
108 Some(b) => {
109 if b.data_after_hex != latest_deltas[key].data_after_hex
110 || b.lamports_after != latest_deltas[key].lamports_after
111 {
112 changed_accounts.push(key.clone());
113 }
114 }
115 }
116 }
117
118 Some(TraceDiff {
119 baseline: self.baseline_trace.clone(),
120 total_cu_delta: latest.total_cu as i64 - self.baseline_trace.total_cu as i64,
121 result_changed,
122 changed_accounts,
123 latest,
124 })
125 }
126
127 async fn resolve_mutations<C: HeliusClient>(
129 &self,
130 _client: &C,
131 ) -> Result<Vec<(Pubkey, Account)>, ReplayError> {
132 let idl_cache = IdlCache::default();
133 let mut out: HashMap<Pubkey, Account> = HashMap::new();
134
135 for (pk, mutation) in &self.mutations {
136 let base = out
137 .get(pk)
138 .cloned()
139 .or_else(|| self.state.accounts.get(pk).cloned())
140 .ok_or_else(|| ReplayError::StateReconstruction {
141 step: "mutation_target_missing".into(),
142 detail: format!("account {pk} not in reconstructed state"),
143 })?;
144
145 let mutated = apply_mutation(base, mutation, &idl_cache)?;
146 out.insert(*pk, mutated);
147 }
148
149 Ok(out.into_iter().collect())
150 }
151}
152
153fn apply_mutation(
154 mut account: Account,
155 mutation: &AccountMutation,
156 idl_cache: &IdlCache,
157) -> Result<Account, ReplayError> {
158 match mutation {
159 AccountMutation::Lamports { new_value } => {
160 account.lamports = *new_value;
161 }
162 AccountMutation::Owner { new_value } => {
163 account.owner = new_value
164 .parse()
165 .map_err(|e| ReplayError::Decoder(format!("bad owner pubkey: {e:?}")))?;
166 }
167 AccountMutation::RawBytes { offset, bytes, extend } => {
168 let required = *offset + bytes.len();
169 if required > account.data.len() {
170 if *extend {
171 account.data.resize(required, 0);
172 } else {
173 return Err(ReplayError::Decoder(format!(
174 "raw_bytes mutation exceeds account data length ({}+{} > {})",
175 offset,
176 bytes.len(),
177 account.data.len()
178 )));
179 }
180 }
181 account.data[*offset..*offset + bytes.len()].copy_from_slice(bytes);
182 }
183 AccountMutation::Field { path, new_value } => {
184 let idl = idl_cache
185 .get_local(&account.owner)
186 .ok_or_else(|| ReplayError::InvalidMutationPath {
187 path: path.clone(),
188 type_name: format!("no IDL for owner {}", account.owner),
189 })?;
190 account.data =
191 crate::idl::apply_field_mutation(&idl, &account.data, path, new_value)?;
192 }
193 }
194 Ok(account)
195}