Skip to content
Open
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
118 changes: 118 additions & 0 deletions crates/core/src/decode/function_call_decoder.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
//! `FunctionCallDecoder`
//!
//! Reconstructs the exact function call a Soroban transaction initiated.
//!
//! Every Soroban transaction begins with a Host Function Invocation: the raw
//! XDR command telling the VM which function to run on which contract with
//! which arguments. In the raw XDR, the arguments arrive as an untyped
//! `SCVec` — just a bag of bytes with no field names attached. This module
//! turns that untyped bag back into a labeled, human-readable call, e.g.
//! `transfer(from: "G...", to: "G...", amount: 1000)`.

use crate::decode::scval_to_json::scval_to_json;
use crate::error::GratError;
use crate::spec::ContractSpec;

use serde::Serialize;
use serde_json::Value as JsonValue;
use stellar_xdr::curr::{HostFunction, InvokeContractArgs, ScVal};

/// One decoded, labeled argument: the parameter name from the contract's
/// spec, paired with its JSON-decoded value.
#[derive(Debug, Clone, Serialize)]
pub struct DecodedArgument {
pub name: String,
pub value: JsonValue,
}

/// The fully reconstructed function call: which function was invoked, and
/// its labeled arguments in declaration order.
#[derive(Debug, Clone, Serialize)]
pub struct DecodedFunctionCall {
pub function_name: String,
pub arguments: Vec<DecodedArgument>,
}

impl DecodedFunctionCall {
/// Renders e.g. `transfer(from: "G...", to: "G...", amount: 1000)`
pub fn pretty_print(&self) -> String {
let args = self
.arguments
.iter()
.map(|a| format!("{}: {}", a.name, a.value))
.collect::<Vec<_>>()
.join(", ");
format!("{}({})", self.function_name, args)
}
}

pub struct FunctionCallDecoder;

impl FunctionCallDecoder {
/// Decode a raw `HostFunction` invocation into a labeled function-call
/// summary, using the contract's parsed `ContractSpec` to resolve the
/// target function's signature.
///
/// # Errors
/// - `GratError::ArityMismatch` if the number of raw arguments doesn't
/// match the number of parameters declared for this function.
/// - `GratError::SpecError` if the function name can't be found in the
/// contract's spec.
/// - `GratError::UnsupportedHostFunction` if the host function isn't an
/// `InvokeContract` invocation.
pub fn decode(
host_function: &HostFunction,
spec: &ContractSpec,
) -> Result<DecodedFunctionCall, GratError> {
let invoke_args: &InvokeContractArgs = match host_function {
HostFunction::InvokeContract(args) => args,
other => {
return Err(GratError::UnsupportedHostFunction(format!(
"FunctionCallDecoder only supports InvokeContract host functions, got: {other:?}"
)));
}
};

let function_name =
String::from_utf8_lossy(invoke_args.function_name.as_ref()).into_owned();
let raw_args: &[ScVal] = invoke_args.args.as_slice();

// Look up this function's declared signature in the contract's spec.
let contract_function = spec
.functions
.iter()
.find(|f| f.name == function_name)
.ok_or_else(|| {
GratError::SpecError(format!(
"function '{function_name}' not found in contract spec"
))
})?;

let expected_params = &contract_function.params;

// Arity check first, before we try to zip/decode anything — a
// mismatched count means the zip below would silently truncate,
// which is exactly the failure mode this decoder exists to catch.
if expected_params.len() != raw_args.len() {
return Err(GratError::ArityMismatch {
function_name: function_name.clone(),
expected: expected_params.len(),
actual: raw_args.len(),
});
}

let arguments = expected_params
.iter()
.zip(raw_args.iter())
.map(|((param_name, _param_type), raw_val)| DecodedArgument {
name: param_name.clone(),
value: scval_to_json(raw_val),
})
.collect();

Ok(DecodedFunctionCall {
function_name,
arguments,
})
}
}
2 changes: 2 additions & 0 deletions crates/core/src/decode/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@ pub mod cross_contract;
pub mod decode_context;
pub mod diagnostic;
pub mod event_walker;
pub mod function_call_decoder;
pub mod host_error;
pub mod mappings;
pub mod report;
pub mod scval_to_json;
pub mod walker;


pub use auth::{
AddressCredential, AuthChain, AuthCredential, AuthFunctionKind, AuthInvocation,
AuthorizationType,
Expand Down
15 changes: 13 additions & 2 deletions crates/core/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ pub enum ArchiveErrorKind {
#[error("decompression failed for '{file}': {reason}")]
DecompressionFailed { file: String, reason: String },
}

#[derive(Debug, Error)]
pub enum GratError {
#[error("RPC request timed out after {timeout_secs}s (method: {method})")]
Expand Down Expand Up @@ -86,6 +85,18 @@ pub enum GratError {

#[error("Invalid Contract ID: {0}")]
InvalidContractId(String),

#[error(
"Function '{function_name}' expects {expected} argument(s), but {actual} were provided"
)]
ArityMismatch {
function_name: String,
expected: usize,
actual: usize,
},

#[error("Unsupported host function: {0}")]
UnsupportedHostFunction(String),
}

pub type GratResult<T> = Result<T, GratError>;
pub type GratResult<T> = Result<T, GratError>;
Loading