use std::collections::BTreeMap;
use std::fmt;
use std::sync::Arc;
use serde::de::{SeqAccess, Visitor};
use serde::ser::SerializeStruct;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use thiserror::Error;
#[derive(Debug, Error)]
pub enum SoldbError {
#[error("{0}")]
Message(String),
#[error("")]
AlreadyReported,
}
pub type SoldbResult<T> = Result<T, SoldbError>;
#[derive(Debug, Clone, Eq)]
pub struct TraceStep {
pub pc: u64,
pub op: Word,
pub gas: u64,
pub gas_cost: u64,
pub depth: u64,
pub stack: Vec<Word>,
pub memory: Option<String>,
pub storage: Option<BTreeMap<String, String>>,
pub error: Option<String>,
pub snapshot: StepSnapshot,
}
impl TraceStep {
#[must_use]
pub fn new(
pc: u64,
op: Word,
gas: u64,
gas_cost: u64,
depth: u64,
error: Option<String>,
snapshot: StepSnapshot,
) -> Self {
Self {
pc,
op,
gas,
gas_cost,
depth,
stack: Vec::new(),
memory: None,
storage: None,
error,
snapshot,
}
}
#[must_use]
pub fn snapshot_ref(&self) -> StepSnapshotRef<'_> {
static EMPTY_STORAGE: BTreeMap<String, String> = BTreeMap::new();
static EMPTY_STORAGE_DIFF: BTreeMap<String, StorageChange> = BTreeMap::new();
if !self.snapshot.is_empty() {
return StepSnapshotRef {
stack: &self.snapshot.stack,
memory: self.snapshot.memory.as_deref(),
storage: &self.snapshot.storage,
storage_diff: &self.snapshot.storage_diff,
};
}
StepSnapshotRef {
stack: &self.stack,
memory: self.memory.as_deref(),
storage: self.storage.as_ref().unwrap_or(&EMPTY_STORAGE),
storage_diff: &EMPTY_STORAGE_DIFF,
}
}
#[must_use]
pub fn normalized_snapshot(&self) -> StepSnapshot {
self.snapshot_ref().to_owned_snapshot()
}
fn normalized(mut self) -> Self {
if self.snapshot.is_empty() {
self.snapshot = StepSnapshot {
stack: std::mem::take(&mut self.stack),
memory: self.memory.take().map(Arc::from),
storage: Arc::new(self.storage.take().unwrap_or_default()),
storage_diff: BTreeMap::new(),
};
} else {
self.stack = Vec::new();
self.memory = None;
self.storage = None;
}
self
}
}
impl PartialEq for TraceStep {
fn eq(&self, other: &Self) -> bool {
self.pc == other.pc
&& self.op == other.op
&& self.gas == other.gas
&& self.gas_cost == other.gas_cost
&& self.depth == other.depth
&& self.error == other.error
&& self.snapshot_ref() == other.snapshot_ref()
}
}
impl Serialize for TraceStep {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let snapshot = self.snapshot_ref();
let mut state = serializer.serialize_struct("TraceStep", 10)?;
state.serialize_field("pc", &self.pc)?;
state.serialize_field("op", &self.op)?;
state.serialize_field("gas", &self.gas)?;
state.serialize_field("gas_cost", &self.gas_cost)?;
state.serialize_field("depth", &self.depth)?;
state.serialize_field("stack", snapshot.stack)?;
state.serialize_field("memory", &snapshot.memory)?;
state.serialize_field("storage", snapshot.storage)?;
state.serialize_field("error", &self.error)?;
state.serialize_field("snapshot", &snapshot)?;
state.end()
}
}
#[derive(Deserialize)]
struct TraceStepRepr {
pc: u64,
op: String,
gas: u64,
gas_cost: u64,
depth: u64,
#[serde(default)]
stack: Vec<String>,
#[serde(default)]
memory: Option<String>,
#[serde(default)]
storage: Option<BTreeMap<String, String>>,
#[serde(default)]
error: Option<String>,
#[serde(default)]
snapshot: StepSnapshot,
}
impl<'de> Deserialize<'de> for TraceStep {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let repr = TraceStepRepr::deserialize(deserializer)?;
Ok(TraceStep {
pc: repr.pc,
op: Arc::from(repr.op),
gas: repr.gas,
gas_cost: repr.gas_cost,
depth: repr.depth,
stack: repr.stack.into_iter().map(Arc::from).collect(),
memory: repr.memory,
storage: repr.storage,
error: repr.error,
snapshot: repr.snapshot,
}
.normalized())
}
}
pub type Word = Arc<str>;
#[derive(Debug, Default)]
pub struct WordInterner {
words: std::collections::HashMap<Word, Word>,
}
impl WordInterner {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn intern(&mut self, word: &str) -> Word {
if let Some(shared) = self.words.get(word) {
return Arc::clone(shared);
}
let shared: Word = Arc::from(word);
self.words.insert(Arc::clone(&shared), Arc::clone(&shared));
shared
}
pub fn intern_stack(&mut self, stack: &mut [Word]) {
for word in stack {
*word = self.intern(word);
}
}
#[must_use]
pub fn len(&self) -> usize {
self.words.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.words.is_empty()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub struct StepSnapshotRef<'a> {
pub stack: &'a [Word],
pub memory: Option<&'a str>,
pub storage: &'a BTreeMap<String, String>,
pub storage_diff: &'a BTreeMap<String, StorageChange>,
}
impl StepSnapshotRef<'_> {
#[must_use]
pub fn to_owned_snapshot(&self) -> StepSnapshot {
StepSnapshot {
stack: self.stack.to_vec(),
memory: self.memory.map(Arc::from),
storage: Arc::new(self.storage.clone()),
storage_diff: self.storage_diff.clone(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Deserialize)]
#[serde(from = "StepSnapshotRepr")]
pub struct StepSnapshot {
pub stack: Vec<Word>,
pub memory: Option<Arc<str>>,
pub storage: Arc<BTreeMap<String, String>>,
pub storage_diff: BTreeMap<String, StorageChange>,
}
impl StepSnapshot {
#[must_use]
pub fn new(
stack: Vec<Word>,
memory: Option<String>,
storage: BTreeMap<String, String>,
storage_diff: BTreeMap<String, StorageChange>,
) -> Self {
Self {
stack,
memory: memory.map(Arc::from),
storage: Arc::new(storage),
storage_diff,
}
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.stack.is_empty()
&& self.memory.is_none()
&& self.storage.is_empty()
&& self.storage_diff.is_empty()
}
}
impl Serialize for StepSnapshot {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let mut state = serializer.serialize_struct("StepSnapshot", 4)?;
state.serialize_field("stack", &self.stack)?;
state.serialize_field("memory", &self.memory.as_deref())?;
state.serialize_field("storage", &*self.storage)?;
state.serialize_field("storage_diff", &self.storage_diff)?;
state.end()
}
}
#[derive(Deserialize, Default)]
struct StepSnapshotRepr {
#[serde(default)]
stack: Vec<Word>,
#[serde(default)]
memory: Option<String>,
#[serde(default)]
storage: BTreeMap<String, String>,
#[serde(default)]
storage_diff: BTreeMap<String, StorageChange>,
}
impl From<StepSnapshotRepr> for StepSnapshot {
fn from(repr: StepSnapshotRepr) -> Self {
Self::new(repr.stack, repr.memory, repr.storage, repr.storage_diff)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct StorageChange {
pub before: Option<String>,
pub after: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(from = "TransactionTraceRepr")]
pub struct TransactionTrace {
pub tx_hash: Option<String>,
pub from_addr: String,
pub to_addr: Option<String>,
pub value: String,
pub input_data: String,
pub gas_used: u64,
pub output: String,
pub success: bool,
pub error: Option<String>,
pub debug_trace_available: bool,
pub contract_address: Option<String>,
#[serde(default)]
pub backend: Option<String>,
#[serde(default)]
pub capabilities: TraceCapabilities,
#[serde(default)]
pub artifacts: TraceArtifacts,
pub steps: Vec<TraceStep>,
}
impl TransactionTrace {
pub fn share_unchanged_state(&mut self) {
for index in 1..self.steps.len() {
let (before, after) = self.steps.split_at_mut(index);
share_snapshot(&before[index - 1], &mut after[0]);
}
}
}
#[derive(Deserialize)]
struct TransactionTraceRepr {
tx_hash: Option<String>,
from_addr: String,
to_addr: Option<String>,
value: String,
input_data: String,
gas_used: u64,
output: String,
success: bool,
error: Option<String>,
debug_trace_available: bool,
contract_address: Option<String>,
#[serde(default)]
backend: Option<String>,
#[serde(default)]
capabilities: TraceCapabilities,
#[serde(default)]
artifacts: TraceArtifacts,
#[serde(deserialize_with = "deserialize_shared_steps")]
steps: Vec<TraceStep>,
}
fn deserialize_shared_steps<'de, D: Deserializer<'de>>(
deserializer: D,
) -> Result<Vec<TraceStep>, D::Error> {
struct SharedSteps;
impl<'de> Visitor<'de> for SharedSteps {
type Value = Vec<TraceStep>;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("an array of trace steps")
}
fn visit_seq<A: SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
let mut steps = Vec::with_capacity(seq.size_hint().unwrap_or(0));
let mut words = WordInterner::new();
while let Some(mut step) = seq.next_element::<TraceStep>()? {
if let Some(previous) = steps.last() {
share_snapshot(previous, &mut step);
}
words.intern_stack(&mut step.snapshot.stack);
steps.push(step);
}
Ok(steps)
}
}
deserializer.deserialize_seq(SharedSteps)
}
fn share_snapshot(previous: &TraceStep, step: &mut TraceStep) {
let previous = &previous.snapshot;
let current = &mut step.snapshot;
if let (Some(previous_memory), Some(current_memory)) = (&previous.memory, ¤t.memory) {
if !Arc::ptr_eq(previous_memory, current_memory) && **previous_memory == **current_memory {
current.memory = Some(Arc::clone(previous_memory));
}
}
if !Arc::ptr_eq(&previous.storage, ¤t.storage) && *previous.storage == *current.storage {
current.storage = Arc::clone(&previous.storage);
}
}
impl From<TransactionTraceRepr> for TransactionTrace {
fn from(repr: TransactionTraceRepr) -> Self {
Self {
tx_hash: repr.tx_hash,
from_addr: repr.from_addr,
to_addr: repr.to_addr,
value: repr.value,
input_data: repr.input_data,
gas_used: repr.gas_used,
output: repr.output,
success: repr.success,
error: repr.error,
debug_trace_available: repr.debug_trace_available,
contract_address: repr.contract_address,
backend: repr.backend,
capabilities: repr.capabilities,
artifacts: repr.artifacts,
steps: repr.steps,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct TraceCapabilities {
#[serde(default)]
pub opcode_steps: bool,
#[serde(default)]
pub stack: bool,
#[serde(default)]
pub memory: bool,
#[serde(default)]
pub storage: bool,
#[serde(default)]
pub storage_diff: bool,
#[serde(default)]
pub call_trace: bool,
#[serde(default)]
pub contract_creation: bool,
#[serde(default)]
pub logs: bool,
#[serde(default)]
pub revert_data: bool,
#[serde(default)]
pub gas_details: bool,
#[serde(default)]
pub account_changes: bool,
#[serde(default)]
pub notes: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct TraceArtifacts {
#[serde(default)]
pub calls: Vec<ExecutionCall>,
#[serde(default)]
pub creations: Vec<ContractCreation>,
#[serde(default)]
pub logs: Vec<ExecutionLog>,
#[serde(default)]
pub account_changes: Vec<AccountChange>,
#[serde(default)]
pub gas: Option<GasSummary>,
#[serde(default)]
pub revert_data: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ExecutionCall {
pub id: usize,
pub parent_id: Option<usize>,
pub depth: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub entry_step: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub exit_step: Option<usize>,
pub call_type: String,
pub from: String,
pub to: String,
pub bytecode_address: String,
pub value: String,
pub input: String,
pub gas_limit: u64,
pub gas_used: Option<u64>,
pub output: Option<String>,
pub success: Option<bool>,
pub error: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ContractCreation {
pub id: usize,
pub parent_id: Option<usize>,
pub depth: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub entry_step: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub exit_step: Option<usize>,
pub create_type: String,
pub caller: String,
pub address: Option<String>,
pub value: String,
pub init_code: String,
pub gas_limit: u64,
pub gas_used: Option<u64>,
pub output: Option<String>,
pub success: Option<bool>,
pub error: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ExecutionLog {
pub index: usize,
pub depth: u64,
pub address: String,
pub topics: Vec<String>,
pub data: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AccountChange {
pub depth: u64,
pub kind: String,
pub address: Option<String>,
pub from: Option<String>,
pub to: Option<String>,
pub value: Option<String>,
pub key: Option<String>,
pub previous_value: Option<String>,
pub previous_nonce: Option<u64>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct GasSummary {
pub used: u64,
pub spent: Option<u64>,
pub refunded: Option<u64>,
pub remaining: Option<u64>,
pub limit: Option<u64>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FunctionCall {
pub name: String,
pub selector: String,
pub entry_step: usize,
pub exit_step: Option<usize>,
pub gas_used: u64,
pub depth: u64,
pub call_type: String,
pub contract_address: Option<String>,
}
#[cfg(test)]
mod tests {
#[test]
fn repeated_stack_words_are_stored_once() {
let mut words = super::WordInterner::new();
assert!(words.is_empty());
let first = words.intern("0x2a");
let again = words.intern("0x2a");
let other = words.intern("0x1");
assert_eq!(words.len(), 2);
assert_eq!(&*first, "0x2a");
assert!(Arc::ptr_eq(&first, &again));
assert!(!Arc::ptr_eq(&first, &other));
let mut stack = vec![Arc::from("0x2a"), Arc::from("0x2a"), Arc::from("0x1")];
words.intern_stack(&mut stack);
assert!(Arc::ptr_eq(&stack[0], &stack[1]));
assert!(Arc::ptr_eq(&stack[0], &first));
assert_eq!(words.len(), 2, "nothing new was seen");
}
#[test]
fn a_parsed_trace_shares_its_repeated_stack_words() {
let document = serde_json::json!({
"tx_hash": "0xabc",
"from_addr": "0x1",
"to_addr": "0x2",
"value": "0x0",
"input_data": "0x",
"gas_used": 0,
"output": "0x",
"success": true,
"debug_trace_available": true,
"steps": [
{"pc": 0, "op": "PUSH1", "gas": 1, "gas_cost": 1, "depth": 1,
"stack": ["0x2a", "0x1"]},
{"pc": 2, "op": "ADD", "gas": 0, "gas_cost": 1, "depth": 1,
"stack": ["0x2a", "0x1"]}
]
})
.to_string();
let trace: TransactionTrace = serde_json::from_str(&document).expect("trace");
let first = &trace.steps[0].snapshot.stack;
let second = &trace.steps[1].snapshot.stack;
assert_eq!(words(first), ["0x2a", "0x1"]);
assert!(Arc::ptr_eq(&first[0], &second[0]));
assert!(Arc::ptr_eq(&first[1], &second[1]));
}
fn words(stack: &[super::Word]) -> Vec<&str> {
stack.iter().map(|word| &**word).collect()
}
use std::collections::BTreeMap;
use std::sync::Arc;
use super::{
FunctionCall, StepSnapshot, StorageChange, TraceArtifacts, TraceCapabilities, TraceStep,
TransactionTrace,
};
fn step_with(snapshot: StepSnapshot) -> TraceStep {
TraceStep {
pc: 3,
op: Arc::from("SSTORE"),
gas: 100,
gas_cost: 5,
depth: 1,
stack: vec![Arc::from("0x01"), Arc::from("0x02")],
memory: Some("aabb".to_owned()),
storage: Some(BTreeMap::from([("0x00".to_owned(), "0x2a".to_owned())])),
error: None,
snapshot,
}
}
#[test]
fn borrowed_snapshot_matches_the_owned_one() {
let flat = step_with(StepSnapshot::default());
let populated = step_with(StepSnapshot::new(
vec![Arc::from("0x09")],
Some("ccdd".to_owned()),
BTreeMap::from([("0x01".to_owned(), "0x63".to_owned())]),
BTreeMap::from([(
"0x01".to_owned(),
StorageChange {
before: None,
after: Some("0x63".to_owned()),
},
)]),
));
for step in [&flat, &populated] {
assert_eq!(
step.snapshot_ref().to_owned_snapshot(),
step.normalized_snapshot()
);
assert_eq!(
serde_json::to_string(&step.snapshot_ref()).expect("borrowed snapshot"),
serde_json::to_string(&step.normalized_snapshot()).expect("owned snapshot"),
);
}
assert_eq!(words(flat.snapshot_ref().stack), ["0x01", "0x02"]);
assert_eq!(flat.snapshot_ref().memory, Some("aabb"));
assert!(flat.snapshot_ref().storage_diff.is_empty());
assert_eq!(words(populated.snapshot_ref().stack), ["0x09"]);
assert_eq!(populated.snapshot_ref().memory, Some("ccdd"));
assert_eq!(populated.snapshot_ref().storage_diff.len(), 1);
}
#[test]
fn steps_serialize_in_the_wire_format_and_read_back_stored_once() {
let step = TraceStep::new(
3,
Arc::from("SSTORE"),
100,
5,
1,
None,
StepSnapshot::new(
vec![Arc::from("0x01")],
Some("aabb".to_owned()),
BTreeMap::from([("0x00".to_owned(), "0x2a".to_owned())]),
BTreeMap::new(),
),
);
let json: serde_json::Value = serde_json::to_value(&step).expect("json");
assert_eq!(json["stack"], serde_json::json!(["0x01"]));
assert_eq!(json["memory"], "aabb");
assert_eq!(json["storage"]["0x00"], "0x2a");
assert_eq!(json["snapshot"]["memory"], "aabb");
assert_eq!(json["snapshot"]["storage"]["0x00"], "0x2a");
assert_eq!(json["gas_cost"], 5);
let flat = step_with(StepSnapshot::default());
let flat_json: serde_json::Value = serde_json::to_value(&flat).expect("json");
assert_eq!(
flat_json["snapshot"]["stack"],
serde_json::json!(["0x01", "0x02"])
);
assert_eq!(flat_json["stack"], serde_json::json!(["0x01", "0x02"]));
let restored: TraceStep = serde_json::from_value(json).expect("step");
assert_eq!(restored, step);
assert!(
restored.stack.is_empty() && restored.memory.is_none() && restored.storage.is_none()
);
let legacy: TraceStep = serde_json::from_str(
r#"{"pc":1,"op":"ADD","gas":9,"gas_cost":3,"depth":1,"stack":["0x1"],"memory":"cc","storage":{"0x0":"0x1"},"error":null}"#,
)
.expect("legacy step");
assert_eq!(words(legacy.snapshot_ref().stack), ["0x1"]);
assert_eq!(legacy.snapshot_ref().memory, Some("cc"));
assert_eq!(legacy.snapshot_ref().storage["0x0"], "0x1");
assert!(legacy.stack.is_empty());
assert_eq!(legacy, step_with_legacy_fields());
let shared = TraceStep::new(
4,
Arc::from("POP"),
95,
2,
1,
None,
StepSnapshot {
stack: Vec::new(),
memory: step.snapshot.memory.clone(),
storage: Arc::clone(&step.snapshot.storage),
storage_diff: BTreeMap::new(),
},
);
assert!(Arc::ptr_eq(
&shared.snapshot.storage,
&step.snapshot.storage
));
assert_eq!(shared.snapshot_ref().memory, Some("aabb"));
}
fn step_with_legacy_fields() -> TraceStep {
TraceStep {
pc: 1,
op: Arc::from("ADD"),
gas: 9,
gas_cost: 3,
depth: 1,
stack: vec![Arc::from("0x1")],
memory: Some("cc".to_owned()),
storage: Some(BTreeMap::from([("0x0".to_owned(), "0x1".to_owned())])),
error: None,
snapshot: StepSnapshot::default(),
}
}
#[test]
fn reading_a_trace_back_shares_unchanged_state_between_steps() {
let snapshot = |memory: &str, slot: &str| {
StepSnapshot::new(
Vec::new(),
Some(memory.to_owned()),
BTreeMap::from([("0x0".to_owned(), slot.to_owned())]),
BTreeMap::new(),
)
};
let step = |pc: u64, snapshot: StepSnapshot| {
TraceStep::new(pc, Arc::from("PUSH1"), 1, 1, 1, None, snapshot)
};
let trace = TransactionTrace {
tx_hash: None,
from_addr: "0x1".to_owned(),
to_addr: None,
value: "0x0".to_owned(),
input_data: "0x".to_owned(),
gas_used: 0,
output: "0x".to_owned(),
success: true,
error: None,
debug_trace_available: true,
contract_address: None,
backend: None,
capabilities: TraceCapabilities::default(),
artifacts: TraceArtifacts::default(),
steps: vec![
step(0, snapshot("aa", "0x1")),
step(1, snapshot("aa", "0x1")),
step(2, snapshot("bb", "0x1")),
step(3, snapshot("bb", "0x2")),
],
};
let json = serde_json::to_string(&trace).expect("json");
let restored: TransactionTrace = serde_json::from_str(&json).expect("trace");
assert_eq!(restored, trace);
let memory = |index: usize| {
restored.steps[index]
.snapshot
.memory
.as_ref()
.expect("memory")
};
assert!(Arc::ptr_eq(memory(0), memory(1)));
assert!(!Arc::ptr_eq(memory(1), memory(2)));
assert!(Arc::ptr_eq(memory(2), memory(3)));
let storage = |index: usize| &restored.steps[index].snapshot.storage;
assert!(Arc::ptr_eq(storage(0), storage(2)));
assert!(!Arc::ptr_eq(storage(2), storage(3)));
let mut by_hand = trace.clone();
assert!(!Arc::ptr_eq(
&by_hand.steps[0].snapshot.storage,
&by_hand.steps[1].snapshot.storage
));
by_hand.share_unchanged_state();
assert!(Arc::ptr_eq(
&by_hand.steps[0].snapshot.storage,
&by_hand.steps[1].snapshot.storage
));
}
#[test]
fn core_models_are_serializable() {
let trace = TransactionTrace {
tx_hash: Some("0xabc".to_owned()),
from_addr: "0x1".to_owned(),
to_addr: Some("0x2".to_owned()),
value: "0".to_owned(),
input_data: "0x".to_owned(),
gas_used: 1,
output: "0x".to_owned(),
success: true,
error: None,
steps: vec![TraceStep {
pc: 0,
op: Arc::from("STOP"),
gas: 1,
gas_cost: 0,
depth: 0,
stack: Vec::new(),
memory: None,
storage: None,
error: None,
snapshot: StepSnapshot::default(),
}],
debug_trace_available: true,
contract_address: None,
backend: Some("debug-rpc".to_owned()),
capabilities: TraceCapabilities::default(),
artifacts: TraceArtifacts::default(),
};
let encoded = serde_json::to_string(&trace).expect("trace serializes");
assert!(encoded.contains("\"success\":true"));
let call = FunctionCall {
name: "runtime_dispatcher".to_owned(),
selector: String::new(),
entry_step: 0,
exit_step: Some(0),
gas_used: 1,
depth: 0,
call_type: "entry".to_owned(),
contract_address: Some("0x2".to_owned()),
};
assert_eq!(call.call_type, "entry");
let change = StorageChange {
before: None,
after: Some("0x2a".to_owned()),
};
assert_eq!(change.after.as_deref(), Some("0x2a"));
}
}