1use std::collections::BTreeMap;
17
18use serde::{Deserialize, Serialize};
19use thiserror::Error;
20
21#[derive(Debug, Error)]
22pub enum SoldbError {
23 #[error("{0}")]
25 Message(String),
26 #[error("")]
32 AlreadyReported,
33}
34
35pub type SoldbResult<T> = Result<T, SoldbError>;
36
37#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
38pub struct TraceStep {
39 pub pc: u64,
40 pub op: String,
41 pub gas: u64,
42 pub gas_cost: u64,
43 pub depth: u64,
44 pub stack: Vec<String>,
45 pub memory: Option<String>,
46 pub storage: Option<BTreeMap<String, String>>,
47 pub error: Option<String>,
48 #[serde(default)]
49 pub snapshot: StepSnapshot,
50}
51
52impl TraceStep {
53 #[must_use]
64 pub fn snapshot_ref(&self) -> StepSnapshotRef<'_> {
65 static EMPTY_STORAGE: BTreeMap<String, String> = BTreeMap::new();
66 static EMPTY_STORAGE_DIFF: BTreeMap<String, StorageChange> = BTreeMap::new();
67
68 if !self.snapshot.is_empty() {
69 return StepSnapshotRef {
70 stack: &self.snapshot.stack,
71 memory: self.snapshot.memory.as_deref(),
72 storage: &self.snapshot.storage,
73 storage_diff: &self.snapshot.storage_diff,
74 };
75 }
76 StepSnapshotRef {
77 stack: &self.stack,
78 memory: self.memory.as_deref(),
79 storage: self.storage.as_ref().unwrap_or(&EMPTY_STORAGE),
80 storage_diff: &EMPTY_STORAGE_DIFF,
81 }
82 }
83
84 #[must_use]
89 pub fn normalized_snapshot(&self) -> StepSnapshot {
90 self.snapshot_ref().to_owned_snapshot()
91 }
92}
93
94#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
99pub struct StepSnapshotRef<'a> {
100 pub stack: &'a [String],
101 pub memory: Option<&'a str>,
102 pub storage: &'a BTreeMap<String, String>,
103 pub storage_diff: &'a BTreeMap<String, StorageChange>,
104}
105
106impl StepSnapshotRef<'_> {
107 #[must_use]
109 pub fn to_owned_snapshot(&self) -> StepSnapshot {
110 StepSnapshot {
111 stack: self.stack.to_vec(),
112 memory: self.memory.map(str::to_owned),
113 storage: self.storage.clone(),
114 storage_diff: self.storage_diff.clone(),
115 }
116 }
117}
118
119#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
120pub struct StepSnapshot {
121 #[serde(default)]
122 pub stack: Vec<String>,
123 #[serde(default)]
124 pub memory: Option<String>,
125 #[serde(default)]
126 pub storage: BTreeMap<String, String>,
127 #[serde(default)]
128 pub storage_diff: BTreeMap<String, StorageChange>,
129}
130
131impl StepSnapshot {
132 #[must_use]
133 pub fn is_empty(&self) -> bool {
134 self.stack.is_empty()
135 && self.memory.is_none()
136 && self.storage.is_empty()
137 && self.storage_diff.is_empty()
138 }
139}
140
141#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
142pub struct StorageChange {
143 pub before: Option<String>,
144 pub after: Option<String>,
145}
146
147#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
148pub struct TransactionTrace {
149 pub tx_hash: Option<String>,
150 pub from_addr: String,
151 pub to_addr: Option<String>,
152 pub value: String,
153 pub input_data: String,
154 pub gas_used: u64,
155 pub output: String,
156 pub success: bool,
157 pub error: Option<String>,
158 pub debug_trace_available: bool,
159 pub contract_address: Option<String>,
160 #[serde(default)]
161 pub backend: Option<String>,
162 #[serde(default)]
163 pub capabilities: TraceCapabilities,
164 #[serde(default)]
165 pub artifacts: TraceArtifacts,
166 pub steps: Vec<TraceStep>,
167}
168
169#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
170pub struct TraceCapabilities {
171 #[serde(default)]
172 pub opcode_steps: bool,
173 #[serde(default)]
174 pub stack: bool,
175 #[serde(default)]
176 pub memory: bool,
177 #[serde(default)]
178 pub storage: bool,
179 #[serde(default)]
180 pub storage_diff: bool,
181 #[serde(default)]
182 pub call_trace: bool,
183 #[serde(default)]
184 pub contract_creation: bool,
185 #[serde(default)]
186 pub logs: bool,
187 #[serde(default)]
188 pub revert_data: bool,
189 #[serde(default)]
190 pub gas_details: bool,
191 #[serde(default)]
192 pub account_changes: bool,
193 #[serde(default)]
194 pub notes: Vec<String>,
195}
196
197#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
198pub struct TraceArtifacts {
199 #[serde(default)]
200 pub calls: Vec<ExecutionCall>,
201 #[serde(default)]
202 pub creations: Vec<ContractCreation>,
203 #[serde(default)]
204 pub logs: Vec<ExecutionLog>,
205 #[serde(default)]
206 pub account_changes: Vec<AccountChange>,
207 #[serde(default)]
208 pub gas: Option<GasSummary>,
209 #[serde(default)]
210 pub revert_data: Option<String>,
211}
212
213#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
214pub struct ExecutionCall {
215 pub id: usize,
216 pub parent_id: Option<usize>,
217 pub depth: u64,
218 #[serde(default, skip_serializing_if = "Option::is_none")]
219 pub entry_step: Option<usize>,
220 #[serde(default, skip_serializing_if = "Option::is_none")]
221 pub exit_step: Option<usize>,
222 pub call_type: String,
223 pub from: String,
224 pub to: String,
225 pub bytecode_address: String,
226 pub value: String,
227 pub input: String,
228 pub gas_limit: u64,
229 pub gas_used: Option<u64>,
230 pub output: Option<String>,
231 pub success: Option<bool>,
232 pub error: Option<String>,
233}
234
235#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
236pub struct ContractCreation {
237 pub id: usize,
238 pub parent_id: Option<usize>,
239 pub depth: u64,
240 #[serde(default, skip_serializing_if = "Option::is_none")]
241 pub entry_step: Option<usize>,
242 #[serde(default, skip_serializing_if = "Option::is_none")]
243 pub exit_step: Option<usize>,
244 pub create_type: String,
245 pub caller: String,
246 pub address: Option<String>,
247 pub value: String,
248 pub init_code: String,
249 pub gas_limit: u64,
250 pub gas_used: Option<u64>,
251 pub output: Option<String>,
252 pub success: Option<bool>,
253 pub error: Option<String>,
254}
255
256#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
257pub struct ExecutionLog {
258 pub index: usize,
259 pub depth: u64,
260 pub address: String,
261 pub topics: Vec<String>,
262 pub data: String,
263}
264
265#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
266pub struct AccountChange {
267 pub depth: u64,
268 pub kind: String,
269 pub address: Option<String>,
270 pub from: Option<String>,
271 pub to: Option<String>,
272 pub value: Option<String>,
273 pub key: Option<String>,
274 pub previous_value: Option<String>,
275 pub previous_nonce: Option<u64>,
276}
277
278#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
279pub struct GasSummary {
280 pub used: u64,
281 pub spent: Option<u64>,
282 pub refunded: Option<u64>,
283 pub remaining: Option<u64>,
284 pub limit: Option<u64>,
285}
286
287#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
288pub struct FunctionCall {
289 pub name: String,
290 pub selector: String,
291 pub entry_step: usize,
292 pub exit_step: Option<usize>,
293 pub gas_used: u64,
294 pub depth: u64,
295 pub call_type: String,
296 pub contract_address: Option<String>,
297}
298
299#[cfg(test)]
300mod tests {
301 use std::collections::BTreeMap;
302
303 use super::{
304 FunctionCall, StepSnapshot, StorageChange, TraceArtifacts, TraceCapabilities, TraceStep,
305 TransactionTrace,
306 };
307
308 fn step_with(snapshot: StepSnapshot) -> TraceStep {
309 TraceStep {
310 pc: 3,
311 op: "SSTORE".to_owned(),
312 gas: 100,
313 gas_cost: 5,
314 depth: 1,
315 stack: vec!["0x01".to_owned(), "0x02".to_owned()],
316 memory: Some("aabb".to_owned()),
317 storage: Some(BTreeMap::from([("0x00".to_owned(), "0x2a".to_owned())])),
318 error: None,
319 snapshot,
320 }
321 }
322
323 #[test]
324 fn borrowed_snapshot_matches_the_owned_one() {
325 let flat = step_with(StepSnapshot::default());
330 let populated = step_with(StepSnapshot {
331 stack: vec!["0x09".to_owned()],
332 memory: Some("ccdd".to_owned()),
333 storage: BTreeMap::from([("0x01".to_owned(), "0x63".to_owned())]),
334 storage_diff: BTreeMap::from([(
335 "0x01".to_owned(),
336 StorageChange {
337 before: None,
338 after: Some("0x63".to_owned()),
339 },
340 )]),
341 });
342
343 for step in [&flat, &populated] {
344 assert_eq!(
345 step.snapshot_ref().to_owned_snapshot(),
346 step.normalized_snapshot()
347 );
348 assert_eq!(
349 serde_json::to_string(&step.snapshot_ref()).expect("borrowed snapshot"),
350 serde_json::to_string(&step.normalized_snapshot()).expect("owned snapshot"),
351 );
352 }
353
354 assert_eq!(flat.snapshot_ref().stack, ["0x01", "0x02"]);
356 assert_eq!(flat.snapshot_ref().memory, Some("aabb"));
357 assert!(flat.snapshot_ref().storage_diff.is_empty());
358
359 assert_eq!(populated.snapshot_ref().stack, ["0x09"]);
361 assert_eq!(populated.snapshot_ref().memory, Some("ccdd"));
362 assert_eq!(populated.snapshot_ref().storage_diff.len(), 1);
363 }
364
365 #[test]
366 fn core_models_are_serializable() {
367 let trace = TransactionTrace {
368 tx_hash: Some("0xabc".to_owned()),
369 from_addr: "0x1".to_owned(),
370 to_addr: Some("0x2".to_owned()),
371 value: "0".to_owned(),
372 input_data: "0x".to_owned(),
373 gas_used: 1,
374 output: "0x".to_owned(),
375 success: true,
376 error: None,
377 steps: vec![TraceStep {
378 pc: 0,
379 op: "STOP".to_owned(),
380 gas: 1,
381 gas_cost: 0,
382 depth: 0,
383 stack: Vec::new(),
384 memory: None,
385 storage: None,
386 error: None,
387 snapshot: StepSnapshot::default(),
388 }],
389 debug_trace_available: true,
390 contract_address: None,
391 backend: Some("debug-rpc".to_owned()),
392 capabilities: TraceCapabilities::default(),
393 artifacts: TraceArtifacts::default(),
394 };
395
396 let encoded = serde_json::to_string(&trace).expect("trace serializes");
397 assert!(encoded.contains("\"success\":true"));
398
399 let call = FunctionCall {
400 name: "runtime_dispatcher".to_owned(),
401 selector: String::new(),
402 entry_step: 0,
403 exit_step: Some(0),
404 gas_used: 1,
405 depth: 0,
406 call_type: "entry".to_owned(),
407 contract_address: Some("0x2".to_owned()),
408 };
409 assert_eq!(call.call_type, "entry");
410
411 let change = StorageChange {
412 before: None,
413 after: Some("0x2a".to_owned()),
414 };
415 assert_eq!(change.after.as_deref(), Some("0x2a"));
416 }
417}