Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
40 changes: 34 additions & 6 deletions crates/core/src/decode/context.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
use crate::decode::auth::{AuthChain, AuthCredential};
use crate::decode::auth_signature::decode_auth_entry_signatures;
use crate::decode::return_decoder::ReturnValueDecoder;
use crate::error::GratResult;
use crate::spec::decoder::ContractSpec;
use crate::types::report::{
AuthEntryInfo, DiagnosticReport, FeeBreakdown, ResourceSummary, TransactionContext,
};
use crate::xdr::codec::XdrCodec;
use stellar_xdr::curr::{TransactionEnvelope, TransactionMeta, TransactionResult};

pub fn enrich_report(report: &mut DiagnosticReport, tx_data: &serde_json::Value) -> GratResult<()> {
enrich_report_with_spec(report, tx_data, None)
}

pub fn enrich_report_with_spec(
report: &mut DiagnosticReport,
tx_data: &serde_json::Value,
contract_spec: Option<&ContractSpec>,
) -> GratResult<()> {
let tx_hash = tx_data
.get("hash")
.and_then(|h| h.as_str())
Expand All @@ -24,7 +34,7 @@ pub fn enrich_report(report: &mut DiagnosticReport, tx_data: &serde_json::Value)
ledger_sequence,
function_name: extract_function_name(tx_data),
arguments: extract_arguments(tx_data),
return_value: extract_return_value(tx_data),
return_value: extract_return_value(tx_data, contract_spec),
fee: extract_fee_breakdown(tx_data),
resources: extract_resource_summary(tx_data),
};
Expand Down Expand Up @@ -53,11 +63,29 @@ fn extract_arguments(tx_data: &serde_json::Value) -> Vec<String> {
.unwrap_or_default()
}

fn extract_return_value(tx_data: &serde_json::Value) -> Option<String> {
tx_data
.get("returnValue")
.and_then(|r| r.as_str())
.map(std::string::ToString::to_string)
fn extract_return_value(
tx_data: &serde_json::Value,
contract_spec: Option<&ContractSpec>,
) -> Option<String> {
let ret_val_str = tx_data.get("returnValue").and_then(|r| r.as_str())?;

if let Ok(sc_val) = stellar_xdr::curr::ScVal::from_xdr_base64(ret_val_str) {
let func_name = extract_function_name(tx_data);
let return_decoder = ReturnValueDecoder::new();

let type_def = if let (Some(cs), Some(fname)) = (contract_spec, func_name.as_deref()) {
cs.functions
.iter()
.find(|f| f.name == fname)
.and_then(|f| f.return_type_def.as_ref())
} else {
None
};

Some(return_decoder.decode_to_string(&sc_val, type_def, contract_spec))
} else {
Some(ret_val_str.to_string())
}
}

fn extract_fee_breakdown(tx_data: &serde_json::Value) -> FeeBreakdown {
Expand Down
166 changes: 166 additions & 0 deletions crates/core/src/decode/function_call_decoder.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
use crate::decode::return_decoder::ReturnValueDecoder;
use crate::spec::decoder::{ContractFunction, ContractSpec};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use stellar_xdr::curr::ScVal;

/// A fully decoded representation of a Soroban contract function invocation.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DecodedFunctionCall {
pub function_name: String,

pub arguments: Vec<Value>,

pub formatted_arguments: Vec<String>,

pub return_value: Option<Value>,

pub formatted_return_value: Option<String>,
}

/// Decoder for contract function calls, handling argument list decoding
/// and delegating return value decoding to `ReturnValueDecoder`.
#[derive(Debug, Clone, Default)]
pub struct FunctionCallDecoder {
return_decoder: ReturnValueDecoder,
}

impl FunctionCallDecoder {
pub fn new() -> Self {
Self {
return_decoder: ReturnValueDecoder::new(),
}
}

/// Decodes function call arguments into a vector of typed JSON values.
pub fn decode_call_arguments(
&self,
args: &[ScVal],
func: &ContractFunction,
contract_spec: Option<&ContractSpec>,
) -> Vec<Value> {
args.iter()
.enumerate()
.map(|(i, arg)| {
let type_def = func.param_defs.get(i).map(|(_, td)| td);
self.return_decoder.decode(arg, type_def, contract_spec)
})
.collect()
}

/// Decodes a function return value using the function's return specification.
pub fn decode_return_value(
&self,
return_val: &ScVal,
func: &ContractFunction,
contract_spec: Option<&ContractSpec>,
) -> Value {
self.return_decoder
.decode_function_return(return_val, func, contract_spec)
}

/// Decodes a full function call (name, arguments, return value) using a `ContractSpec`.
pub fn decode_function_call(
&self,
func_name: &str,
args: &[ScVal],
return_val: Option<&ScVal>,
contract_spec: &ContractSpec,
) -> DecodedFunctionCall {
let matching_func = contract_spec.functions.iter().find(|f| f.name == func_name);

let (arguments, formatted_arguments) = if let Some(func) = matching_func {
let decoded_args = self.decode_call_arguments(args, func, Some(contract_spec));
let formatted: Vec<String> = decoded_args
.iter()
.map(|v| match v {
Value::String(s) => s.clone(),
other => serde_json::to_string(other).unwrap_or_else(|_| other.to_string()),
})
.collect();
(decoded_args, formatted)
} else {
let decoded_args: Vec<Value> = args.iter().map(ReturnValueDecoder::decode_dynamic).collect();
let formatted: Vec<String> = decoded_args
.iter()
.map(|v| match v {
Value::String(s) => s.clone(),
other => serde_json::to_string(other).unwrap_or_else(|_| other.to_string()),
})
.collect();
(decoded_args, formatted)
};

let (return_value, formatted_return_value) = match return_val {
Some(rv) => {
let val = if let Some(func) = matching_func {
self.decode_return_value(rv, func, Some(contract_spec))
} else {
ReturnValueDecoder::decode_dynamic(rv)
};
let formatted = match &val {
Value::String(s) => s.clone(),
other => serde_json::to_string(other).unwrap_or_else(|_| other.to_string()),
};
(Some(val), Some(formatted))
}
None => (None, None),
};

DecodedFunctionCall {
function_name: func_name.to_string(),
arguments,
formatted_arguments,
return_value,
formatted_return_value,
}
}
}

#[cfg(test)]
mod tests {
use super::*;
use stellar_xdr::curr::{ScSpecTypeDef, ScSymbol};

#[test]
fn test_decode_function_call() {
let decoder = FunctionCallDecoder::new();

let func = ContractFunction {
name: "transfer".to_string(),
params: vec![
("to".to_string(), "Address".to_string()),
("amount".to_string(), "I128".to_string()),
],
return_type: "Bool".to_string(),
doc: None,
return_type_def: Some(ScSpecTypeDef::Bool),
param_defs: vec![
("to".to_string(), ScSpecTypeDef::Address),
("amount".to_string(), ScSpecTypeDef::I128),
],
};

let spec = ContractSpec {
errors: vec![],
functions: vec![func],
structs: vec![],
name: None,
version: None,
enums: vec![],
unions: vec![],
};

let args = vec![
ScVal::Symbol(ScSymbol("recipient".try_into().unwrap())),
ScVal::I32(500),
];
let return_val = ScVal::Bool(true);

let decoded = decoder.decode_function_call("transfer", &args, Some(&return_val), &spec);
assert_eq!(decoded.function_name, "transfer");
assert_eq!(decoded.arguments.len(), 2);
assert_eq!(decoded.return_value, Some(serde_json::json!(true)));
assert_eq!(decoded.formatted_return_value, Some("true".to_string()));
Comment on lines +154 to +164

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Test doesn't exercise typed argument decoding.

The args (Symbol, I32) don't match the declared param_defs (Address, I128), so both fall through to the dynamic path, and the only assertion is arguments.len() == 2. Use a matching ScVal::Address / ScVal::I128 pair and assert the decoded values.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/core/src/decode/function_call_decoder.rs` around lines 154 - 164,
Update the decode_function_call test inputs to match the declared Address and
I128 parameter definitions, replacing the Symbol and I32 ScVals with
corresponding typed values. Add assertions that verify the decoded argument
contents, while preserving the existing function name, return value, and
formatted return value checks.

}
}
4 changes: 4 additions & 0 deletions crates/core/src/decode/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,20 @@ pub mod contract_error;
pub mod cross_contract;
pub mod decode_context;
pub mod diagnostic;
pub mod function_call_decoder;
pub mod host_error;
pub mod mappings;
pub mod report;
pub mod return_decoder;
pub mod walker;

pub use auth::{
AddressCredential, AuthChain, AuthCredential, AuthFunctionKind, AuthInvocation,
AuthorizationType,
};
pub use auth_address_nonce::AddressWithNonce;
pub use function_call_decoder::{DecodedFunctionCall, FunctionCallDecoder};
pub use return_decoder::ReturnValueDecoder;
Comment on lines +27 to +28

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Restore FunctionCallDecoder or remove the stale re-export.

The current change removes the FunctionCallDecoder implementation, while this module still re-exports FunctionCallDecoder at Line 27-28. If no replacement definition exists, grat-core will fail to compile. Keep the implementation or update the public API and all callers together.

#!/usr/bin/env bash
set -euo pipefail
rg -n 'FunctionCallDecoder' \
  crates/core/src/decode/function_call_decoder.rs \
  crates/core/src/decode/mod.rs
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/core/src/decode/mod.rs` around lines 27 - 28, Resolve the stale
FunctionCallDecoder API in the decode module: either restore the
FunctionCallDecoder implementation in function_call_decoder.rs, or remove its
re-export from decode/mod.rs and update every remaining caller and public API
reference consistently. Ensure grat-core compiles without unresolved
FunctionCallDecoder symbols.

pub use walker::{
walk_diagnostic_events, DiagnosticEventKind, DiagnosticEventWalker, StructuredDiagnosticEvent,
};
Expand Down
Loading