diff --git a/crates/core/src/decode/context.rs b/crates/core/src/decode/context.rs index 6f7eea48..f366f73b 100644 --- a/crates/core/src/decode/context.rs +++ b/crates/core/src/decode/context.rs @@ -20,6 +20,7 @@ pub fn enrich_report( ledger_sequence, function_name: extract_function_name(tx_data), arguments: extract_arguments(tx_data), + return_value: extract_return_value(tx_data), fee: extract_fee_breakdown(tx_data), resources: extract_resource_summary(tx_data), }; @@ -43,6 +44,13 @@ fn extract_arguments(tx_data: &serde_json::Value) -> Vec { .unwrap_or_default() } +fn extract_return_value(tx_data: &serde_json::Value) -> Option { + tx_data + .get("returnValue") + .and_then(|r| r.as_str()) + .map(std::string::ToString::to_string) +} + fn extract_fee_breakdown(tx_data: &serde_json::Value) -> FeeBreakdown { FeeBreakdown { inclusion_fee: tx_data diff --git a/crates/core/src/decode/mod.rs b/crates/core/src/decode/mod.rs index f2ba8310..03834a59 100644 --- a/crates/core/src/decode/mod.rs +++ b/crates/core/src/decode/mod.rs @@ -9,6 +9,70 @@ pub mod report; use crate::error::PrismResult; use crate::types::report::DiagnosticReport; +use crate::xdr::codec::XdrCodec; +use stellar_xdr::curr::{ScVal, TransactionMeta, TransactionResult}; + +/// Decode `resultMetaXdr` as `TransactionMeta` and, if it is V3, inject the +/// Soroban contract events, diagnostic events, and return value into the JSON +/// payload so downstream enrichment code sees the same shape it does for V1/V2. +/// +/// Also extracts `fee_charged` from `resultXdr` so fee details are not lost. +fn parse_v3_metadata(tx_data: &mut serde_json::Value) -> PrismResult<()> { + // Inject fee_charged from TransactionResult regardless of V3. + if let Some(result_b64) = tx_data.get("resultXdr").and_then(|r| r.as_str()) { + if let Ok(tx_result) = TransactionResult::from_xdr_base64(result_b64) { + tx_data["inclusionFee"] = serde_json::json!(tx_result.fee_charged); + } + } + + let meta_b64 = match tx_data.get("resultMetaXdr").and_then(|r| r.as_str()) { + Some(s) => s.to_string(), + None => return Ok(()), + }; + + let meta = match TransactionMeta::from_xdr_base64(&meta_b64) { + Ok(m) => m, + Err(_) => return Ok(()), + }; + + if let TransactionMeta::V3(v3) = meta { + let soroban_meta = match v3.soroban_meta { + Some(s) => s, + None => return Ok(()), + }; + + // Inject contract events as base64 XDR strings. + if !soroban_meta.events.is_empty() { + let contract_events: Vec = soroban_meta + .events + .iter() + .filter_map(|e| XdrCodec::to_xdr_base64(e).ok()) + .collect(); + tx_data["events"] = serde_json::json!({ + "contractEventsXdr": contract_events + }); + } + + // Inject diagnostic events as base64 XDR strings. + if !soroban_meta.diagnostic_events.is_empty() { + let diagnostic_events: Vec = soroban_meta + .diagnostic_events + .iter() + .filter_map(|e| XdrCodec::to_xdr_base64(e).ok()) + .collect(); + tx_data["diagnosticEventsXdr"] = serde_json::json!(diagnostic_events); + } + + // Encode the return value as a base64 XDR string. + if soroban_meta.return_value != ScVal::Void { + if let Ok(b64) = XdrCodec::to_xdr_base64(&soroban_meta.return_value) { + tx_data["returnValue"] = serde_json::json!(b64); + } + } + } + + Ok(()) +} fn filter_transaction_by_operation( tx_data: &mut serde_json::Value, @@ -58,6 +122,8 @@ pub async fn decode_transaction_with_op_filter( let mut tx_data = serde_json::to_value(tx_data) .map_err(|e| crate::error::PrismError::Internal(e.to_string()))?; + parse_v3_metadata(&mut tx_data)?; + if let Some(index) = op_index { filter_transaction_by_operation(&mut tx_data, index)?; } diff --git a/crates/core/src/types/report.rs b/crates/core/src/types/report.rs index 232f3d47..4355786f 100644 --- a/crates/core/src/types/report.rs +++ b/crates/core/src/types/report.rs @@ -58,6 +58,8 @@ pub struct TransactionContext { pub arguments: Vec, + pub return_value: Option, + pub fee: FeeBreakdown, pub resources: ResourceSummary, diff --git a/crates/core/src/xdr/codec.rs b/crates/core/src/xdr/codec.rs index f0d6cf2e..dc655467 100644 --- a/crates/core/src/xdr/codec.rs +++ b/crates/core/src/xdr/codec.rs @@ -3,8 +3,8 @@ use crate::error::{PrismError, PrismResult}; use base64::{engine::general_purpose::STANDARD, Engine as _}; use stellar_xdr::curr::{ - DiagnosticEvent, LedgerEntry, Limits, ReadXdr, ScVec, TransactionEnvelope, TransactionMeta, - WriteXdr, TransactionResult, + ContractEvent, DiagnosticEvent, LedgerEntry, Limits, ReadXdr, ScVal, ScVec, + TransactionEnvelope, TransactionMeta, WriteXdr, TransactionResult, }; pub trait XdrCodec: Sized { @@ -144,6 +144,44 @@ impl XdrCodec for ScVec { } } +impl XdrCodec for ContractEvent { + const TYPE_NAME: &'static str = "ContractEvent"; + + fn from_xdr_bytes(bytes: &[u8]) -> PrismResult { + ContractEvent::from_xdr(bytes, Limits::none()).map_err(|e| { + PrismError::XdrDecodingFailed { + type_name: Self::TYPE_NAME, + reason: e.to_string(), + } + }) + } + + fn to_xdr_bytes(&self) -> PrismResult> { + self.to_xdr(Limits::none()).map_err(|e| { + PrismError::XdrError(format!("Failed to encode {}: {}", Self::TYPE_NAME, e)) + }) + } +} + +impl XdrCodec for ScVal { + const TYPE_NAME: &'static str = "ScVal"; + + fn from_xdr_bytes(bytes: &[u8]) -> PrismResult { + ScVal::from_xdr(bytes, Limits::none()).map_err(|e| { + PrismError::XdrDecodingFailed { + type_name: Self::TYPE_NAME, + reason: e.to_string(), + } + }) + } + + fn to_xdr_bytes(&self) -> PrismResult> { + self.to_xdr(Limits::none()).map_err(|e| { + PrismError::XdrError(format!("Failed to encode {}: {}", Self::TYPE_NAME, e)) + }) + } +} + /// Decode a base64-encoded XDR string to raw bytes. pub fn decode_xdr_base64(xdr_base64: &str) -> PrismResult> { STANDARD.decode(xdr_base64).map_err(|e| {