Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: use JSON.stringify for JS result #71

Merged
merged 1 commit into from
Mar 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 58 additions & 1 deletion src/tracing/js/bindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -969,7 +969,9 @@ where
#[cfg(test)]
mod tests {
use super::*;
use crate::tracing::js::builtins::BIG_INT_JS;
use crate::tracing::js::builtins::{
json_stringify, register_builtins, to_serde_value, BIG_INT_JS,
};
use boa_engine::{property::Attribute, Source};
use revm::db::{CacheDB, EmptyDB};

Expand Down Expand Up @@ -1135,4 +1137,59 @@ mod tests {
assert!(res.is_err());
}
}

#[test]
fn test_big_int() {
let mut context = Context::default();
register_builtins(&mut context).unwrap();

let eval = context
.eval(Source::from_bytes(
r#"({data: [], fault: function(log) {}, step: function(log) { this.data.push({ value: log.stack.peek(2) }) }, result: function() { return this.data; }})"#
.to_string()
.as_bytes(),
))
.unwrap();

let obj = eval.as_object().unwrap();

let result_fn =
obj.get(js_string!("result"), &mut context).unwrap().as_object().cloned().unwrap();
let step_fn =
obj.get(js_string!("step"), &mut context).unwrap().as_object().cloned().unwrap();

let mut stack = Stack::new();
stack.push(U256::from(35000)).unwrap();
stack.push(U256::from(35000)).unwrap();
stack.push(U256::from(35000)).unwrap();
let (stack_ref, _stack_guard) = StackRef::new(&stack);
let mem = SharedMemory::new();
let (mem_ref, _mem_guard) = MemoryRef::new(&mem);

let step = StepLog {
stack: stack_ref,
op: OpObj(0),
memory: mem_ref,
pc: 0,
gas_remaining: 0,
cost: 0,
depth: 0,
refund: 0,
error: None,
contract: Default::default(),
};

let js_step = step.into_js_object(&mut context).unwrap();

let _ = step_fn.call(&eval, &[js_step.into()], &mut context).unwrap();

let res = result_fn.call(&eval, &[], &mut context).unwrap();
let val = json_stringify(res.clone(), &mut context).unwrap().to_std_string().unwrap();
assert_eq!(val, r#"[{"value":"35000"}]"#);

let val = to_serde_value(res, &mut context).unwrap();
assert!(val.is_array());
let s = val.to_string();
assert_eq!(s, r#"[{"value":"35000"}]"#);
}
}
42 changes: 42 additions & 0 deletions src/tracing/js/builtins.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,48 @@ use std::{borrow::Borrow, collections::HashSet};
/// bigIntegerJS is the minified version of <https://github.com/peterolson/BigInteger.js>.
pub(crate) const BIG_INT_JS: &str = include_str!("bigint.js");

/// Converts the given `JsValue` to a `serde_json::Value`.
///
/// This first attempts to use the built-in `JSON.stringify` function to convert the value to a JSON
///
/// If that fails it uses boa's to_json function to convert the value to a JSON object
///
/// We use `JSON.stringify` so that `toJSON` properties are used when converting the value to JSON,
/// this ensures the `bigint` is serialized properly.
pub(crate) fn to_serde_value(val: JsValue, ctx: &mut Context) -> JsResult<serde_json::Value> {
if let Ok(json) = json_stringify(val.clone(), ctx) {
let json = json.to_std_string().map_err(|err| {
JsError::from_native(
JsNativeError::error()
.with_message(format!("failed to convert JSON to string: {}", err)),
)
})?;
serde_json::from_str(&json).map_err(|err| {
JsError::from_native(
JsNativeError::error().with_message(format!("failed to parse JSON: {}", err)),
)
})
} else {
val.to_json(ctx)
}
}

/// Attempts to use the global `JSON` object to stringify the given value.
pub(crate) fn json_stringify(val: JsValue, ctx: &mut Context) -> JsResult<JsString> {
let json = ctx.global_object().get(js_string!("JSON"), ctx)?;
let json_obj = json.as_object().ok_or_else(|| {
JsError::from_native(JsNativeError::typ().with_message("JSON is not an object"))
})?;

let stringify = json_obj.get(js_string!("stringify"), ctx)?;

let stringify = stringify.as_callable().ok_or_else(|| {
JsError::from_native(JsNativeError::typ().with_message("JSON.stringify is not callable"))
})?;
let res = stringify.call(&json, &[val], ctx)?;
res.to_string(ctx)
}

/// Registers all the builtin functions and global bigint property
///
/// Note: this does not register the `isPrecompiled` builtin, as this requires the precompile
Expand Down
5 changes: 3 additions & 2 deletions src/tracing/js/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use crate::tracing::{
bindings::{
CallFrame, Contract, EvmDbRef, FrameResult, JsEvmContext, MemoryRef, StackRef, StepLog,
},
builtins::{register_builtins, PrecompileList},
builtins::{register_builtins, to_serde_value, PrecompileList},
},
types::CallKind,
};
Expand Down Expand Up @@ -214,7 +214,8 @@ impl JsInspector {
DB: DatabaseRef,
<DB as DatabaseRef>::Error: std::fmt::Display,
{
Ok(self.result(res, env, db)?.to_json(&mut self.ctx)?)
let result = self.result(res, env, db)?;
Ok(to_serde_value(result, &mut self.ctx)?)
}

/// Calls the result function and returns the result.
Expand Down
Loading