From 24ef0deae4d7bc7f89cd97c7294a5f2a6725a400 Mon Sep 17 00:00:00 2001 From: ayo webdev Date: Wed, 30 Sep 2026 12:20:44 +0100 Subject: [PATCH] feat(core): implement XdrDiff for AccountEntry Closes #453 --- crates/core/src/archive/mod.rs | 7 +- crates/core/src/cache/mod.rs | 2 - crates/core/src/cache/store.rs | 3 - crates/core/src/debugger/breakpoint.rs | 22 +- crates/core/src/debugger/mod.rs | 2 - crates/core/src/debugger/stepping.rs | 6 - crates/core/src/debugger/whatif.rs | 9 - crates/core/src/decode/auth.rs | 29 ++- crates/core/src/decode/auth_signature.rs | 2 +- crates/core/src/decode/context.rs | 44 ++-- crates/core/src/decode/contract_error.rs | 5 +- crates/core/src/decode/cross_contract.rs | 13 +- crates/core/src/decode/decode_context.rs | 3 +- crates/core/src/decode/diagnostic.rs | 12 +- crates/core/src/decode/host_error.rs | 88 +++++--- crates/core/src/decode/mappings/auth.rs | 3 - crates/core/src/decode/mappings/budget.rs | 7 +- crates/core/src/decode/mappings/context.rs | 2 - crates/core/src/decode/mappings/mod.rs | 2 - crates/core/src/decode/mappings/object.rs | 9 +- .../src/decode/mappings/severity_tests.rs | 25 ++- crates/core/src/decode/mappings/storage.rs | 7 +- crates/core/src/decode/mappings/value.rs | 7 +- crates/core/src/decode/mod.rs | 30 +-- crates/core/src/decode/walker.rs | 139 +++++++++--- crates/core/src/error.rs | 5 - crates/core/src/lib.rs | 12 +- crates/core/src/network/config.rs | 15 +- crates/core/src/network/mod.rs | 2 - crates/core/src/replay/differ.rs | 4 +- crates/core/src/replay/mod.rs | 4 +- crates/core/src/replay/profiler.rs | 6 +- crates/core/src/replay/sandbox.rs | 8 +- crates/core/src/replay/state.rs | 12 +- crates/core/src/replay/trace.rs | 7 +- crates/core/src/rpc/client.rs | 64 ++++-- crates/core/src/rpc/jsonrpc.rs | 25 ++- crates/core/src/rpc/metrics.rs | 24 +- crates/core/src/spec/decoder.rs | 24 +- crates/core/src/spec/mod.rs | 2 - crates/core/src/taxonomy/mod.rs | 2 - crates/core/src/types/address.rs | 5 - crates/core/src/types/config.rs | 8 +- crates/core/src/types/mod.rs | 2 - crates/core/src/types/report.rs | 8 - crates/core/src/types/trace.rs | 10 - crates/core/src/xdr/codec.rs | 205 +++++++++--------- crates/core/src/xdr/diff.rs | 164 ++++++++++++++ crates/core/src/xdr/mod.rs | 3 +- crates/core/src/xdr/scratch.rs | 8 + 50 files changed, 664 insertions(+), 443 deletions(-) create mode 100644 crates/core/src/xdr/diff.rs create mode 100644 crates/core/src/xdr/scratch.rs diff --git a/crates/core/src/archive/mod.rs b/crates/core/src/archive/mod.rs index cf470ec0..9483c222 100644 --- a/crates/core/src/archive/mod.rs +++ b/crates/core/src/archive/mod.rs @@ -1,10 +1,7 @@ - - -use crate::network::NetworkConfig; use crate::error::{ArchiveErrorKind, PrismResult}; +use crate::network::NetworkConfig; pub struct ArchiveClient { - #[allow(dead_code)] client: reqwest::Client, @@ -14,7 +11,6 @@ pub struct ArchiveClient { #[derive(Debug)] pub struct ArchiveCheckpoint { - pub ledger_sequence: u32, pub ledger_header: Vec, @@ -25,7 +21,6 @@ pub struct ArchiveCheckpoint { } impl ArchiveClient { - pub fn new(config: &NetworkConfig) -> Self { Self { client: reqwest::Client::new(), diff --git a/crates/core/src/cache/mod.rs b/crates/core/src/cache/mod.rs index 5f7091cd..55c88cbf 100644 --- a/crates/core/src/cache/mod.rs +++ b/crates/core/src/cache/mod.rs @@ -1,3 +1 @@ - - pub mod store; diff --git a/crates/core/src/cache/store.rs b/crates/core/src/cache/store.rs index d80104bf..81585e8c 100644 --- a/crates/core/src/cache/store.rs +++ b/crates/core/src/cache/store.rs @@ -1,11 +1,8 @@ - - use crate::error::{PrismError, PrismResult}; use std::path::PathBuf; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum CacheCategory { - WasmBlob, ContractSpec, diff --git a/crates/core/src/debugger/breakpoint.rs b/crates/core/src/debugger/breakpoint.rs index fea6ed7b..cc3995c0 100644 --- a/crates/core/src/debugger/breakpoint.rs +++ b/crates/core/src/debugger/breakpoint.rs @@ -1,10 +1,7 @@ - - use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Breakpoint { - pub id: u32, pub condition: BreakpointCondition, @@ -18,7 +15,6 @@ pub struct Breakpoint { #[derive(Debug, Clone, Serialize, Deserialize)] pub enum BreakpointCondition { - FunctionEntry { contract_id: Option, function_name: String, @@ -29,24 +25,30 @@ pub enum BreakpointCondition { function_name: String, }, - HostFunction { function_name: String }, + HostFunction { + function_name: String, + }, - ContractCall { target_contract_id: String }, + ContractCall { + target_contract_id: String, + }, - BudgetThreshold { cpu_instructions: u64 }, + BudgetThreshold { + cpu_instructions: u64, + }, - StorageAccess { ledger_key: String }, + StorageAccess { + ledger_key: String, + }, } pub struct BreakpointController { - breakpoints: Vec, next_id: u32, } impl BreakpointController { - pub fn new() -> Self { Self { breakpoints: Vec::new(), diff --git a/crates/core/src/debugger/mod.rs b/crates/core/src/debugger/mod.rs index 4bc95330..2d7c56c8 100644 --- a/crates/core/src/debugger/mod.rs +++ b/crates/core/src/debugger/mod.rs @@ -1,5 +1,3 @@ - - pub mod breakpoint; pub mod stepping; pub mod whatif; diff --git a/crates/core/src/debugger/stepping.rs b/crates/core/src/debugger/stepping.rs index ab721377..02cde799 100644 --- a/crates/core/src/debugger/stepping.rs +++ b/crates/core/src/debugger/stepping.rs @@ -1,10 +1,7 @@ - - use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum StepCommand { - StepInto, StepOver, @@ -18,7 +15,6 @@ pub enum StepCommand { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PauseState { - pub trace_position: usize, pub current_contract: String, @@ -37,14 +33,12 @@ pub struct PauseState { } pub struct ExecutionStepper { - current_state: Option, is_paused: bool, } impl ExecutionStepper { - pub fn new() -> Self { Self { current_state: None, diff --git a/crates/core/src/debugger/whatif.rs b/crates/core/src/debugger/whatif.rs index 400f83e7..278deca6 100644 --- a/crates/core/src/debugger/whatif.rs +++ b/crates/core/src/debugger/whatif.rs @@ -1,35 +1,28 @@ - - use crate::error::PrismResult; use crate::types::trace::ExecutionTrace; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize)] pub enum WhatIfPatch { - ModifyArgument { - index: usize, new_value: String, }, ModifyLedgerEntry { - key: String, new_value: String, }, ModifyResourceLimits { - cpu_limit: Option, memory_limit: Option, }, ModifyAuth { - add_signer: Option, remove_signer: Option, @@ -38,7 +31,6 @@ pub enum WhatIfPatch { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct WhatIfResult { - pub original: ExecutionTrace, pub modified: ExecutionTrace, @@ -53,7 +45,6 @@ pub async fn simulate_whatif( _patches: &[WhatIfPatch], _network: &crate::types::config::NetworkConfig, ) -> PrismResult { - Err(crate::error::PrismError::Internal( "What-if simulation not yet implemented".to_string(), )) diff --git a/crates/core/src/decode/auth.rs b/crates/core/src/decode/auth.rs index b32cfd62..28fe495b 100644 --- a/crates/core/src/decode/auth.rs +++ b/crates/core/src/decode/auth.rs @@ -100,7 +100,9 @@ impl AuthChain { fn parse_credential(credentials: &SorobanCredentials) -> AuthCredential { match credentials { SorobanCredentials::SourceAccount => AuthCredential::SourceAccount, - SorobanCredentials::Address(addr) => AuthCredential::Address(parse_address_credential(addr)), + SorobanCredentials::Address(addr) => { + AuthCredential::Address(parse_address_credential(addr)) + } } } @@ -162,7 +164,9 @@ mod tests { use stellar_xdr::curr::{InvokeContractArgs, ScSymbol}; fn account_address(seed: u8) -> ScAddress { - ScAddress::Account(AccountId(PublicKey::PublicKeyTypeEd25519(Uint256([seed; 32])))) + ScAddress::Account(AccountId(PublicKey::PublicKeyTypeEd25519(Uint256( + [seed; 32], + )))) } fn contract_address(seed: u8) -> ScAddress { @@ -248,7 +252,10 @@ mod tests { signature_expiration_ledger: 0, signature: ScVal::Void, }), - root_invocation: invocation(contract_fn(contract_address(2), "f", vec![]), empty_subs()), + root_invocation: invocation( + contract_fn(contract_address(2), "f", vec![]), + empty_subs(), + ), }; let chain = AuthChain::from_entry(&entry); @@ -261,9 +268,16 @@ mod tests { #[test] fn nested_invocations_are_flattened_depth_first() { // root -> [child_a -> [grandchild], child_b] - let grandchild = invocation(contract_fn(contract_address(30), "gc", vec![]), empty_subs()); + let grandchild = invocation( + contract_fn(contract_address(30), "gc", vec![]), + empty_subs(), + ); let child_a = invocation( - contract_fn(contract_address(20), "a", vec![ScVal::U32(1), ScVal::U32(2)]), + contract_fn( + contract_address(20), + "a", + vec![ScVal::U32(1), ScVal::U32(2)], + ), vec![grandchild], ); let child_b = invocation(contract_fn(contract_address(21), "b", vec![]), empty_subs()); @@ -284,10 +298,7 @@ mod tests { .map(|i| (i.depth, i.function.as_deref().unwrap())) .collect(); - assert_eq!( - steps, - vec![(0, "root"), (1, "a"), (2, "gc"), (1, "b")] - ); + assert_eq!(steps, vec![(0, "root"), (1, "a"), (2, "gc"), (1, "b")]); // Arg counts are preserved per step. assert_eq!(chain.invocations[1].arg_count, 2); } diff --git a/crates/core/src/decode/auth_signature.rs b/crates/core/src/decode/auth_signature.rs index 41edb6b0..7d06bd14 100644 --- a/crates/core/src/decode/auth_signature.rs +++ b/crates/core/src/decode/auth_signature.rs @@ -65,7 +65,7 @@ fn extract_signatures_from_scval(val: &ScVal) -> Vec { // Vec of signature entries (e.g., multiple account signatures) ScVal::Vec(Some(vec)) => vec .iter() - .flat_map(|v| extract_signatures_from_scval(v)) + .flat_map(extract_signatures_from_scval) .collect(), _ => vec![], diff --git a/crates/core/src/decode/context.rs b/crates/core/src/decode/context.rs index cfd45c93..ea550d9b 100644 --- a/crates/core/src/decode/context.rs +++ b/crates/core/src/decode/context.rs @@ -1,5 +1,3 @@ - - use crate::decode::auth_signature::decode_auth_entry_signatures; use crate::error::PrismResult; use crate::types::report::{DiagnosticReport, FeeBreakdown, ResourceSummary, TransactionContext}; @@ -14,7 +12,10 @@ pub fn enrich_report( .unwrap_or("unknown") .to_string(); - let ledger_sequence = tx_data.get("ledger").and_then(serde_json::Value::as_u64).unwrap_or(0) as u32; + let ledger_sequence = tx_data + .get("ledger") + .and_then(serde_json::Value::as_u64) + .unwrap_or(0) as u32; let context = TransactionContext { tx_hash, @@ -58,7 +59,7 @@ fn extract_return_value(tx_data: &serde_json::Value) -> Option { fn extract_fee_breakdown(tx_data: &serde_json::Value) -> FeeBreakdown { use crate::xdr::codec::XdrCodec; - use stellar_xdr::curr::{TransactionEnvelope, TransactionResult, TransactionMeta}; + use stellar_xdr::curr::{TransactionEnvelope, TransactionMeta, TransactionResult}; // 1. Get total fee from resultXdr let mut total_fee = 0; @@ -74,13 +75,13 @@ fn extract_fee_breakdown(tx_data: &serde_json::Value) -> FeeBreakdown { if let Ok(tx_envelope) = TransactionEnvelope::from_xdr_base64(envelope_xdr_b64) { match tx_envelope { TransactionEnvelope::Tx(v1) => { - bid_fee = Some(v1.tx.fee as i64); + bid_fee = Some(i64::from(v1.tx.fee)); } TransactionEnvelope::TxFeeBump(fee_bump) => { - bid_fee = Some(fee_bump.tx.fee as i64); + bid_fee = Some(fee_bump.tx.fee); } TransactionEnvelope::TxV0(v0) => { - bid_fee = Some(v0.tx.fee as i64); + bid_fee = Some(i64::from(v0.tx.fee)); } } } @@ -94,21 +95,18 @@ fn extract_fee_breakdown(tx_data: &serde_json::Value) -> FeeBreakdown { if let Some(meta_xdr_b64) = tx_data.get("resultMetaXdr").and_then(|v| v.as_str()) { if let Ok(tx_meta) = TransactionMeta::from_xdr_base64(meta_xdr_b64) { - match tx_meta { - TransactionMeta::V3(v3) => { - if let Some(soroban_meta) = v3.soroban_meta { - match soroban_meta.ext { - stellar_xdr::curr::SorobanTransactionMetaExt::V0 => {} - stellar_xdr::curr::SorobanTransactionMetaExt::V1(v1) => { - non_refundable_fee = v1.total_non_refundable_resource_fee_charged; - refundable_fee = v1.total_refundable_resource_fee_charged; - rent_fee = v1.rent_fee_charged; - has_soroban_meta = true; - } + if let TransactionMeta::V3(v3) = tx_meta { + if let Some(soroban_meta) = v3.soroban_meta { + match soroban_meta.ext { + stellar_xdr::curr::SorobanTransactionMetaExt::V0 => {} + stellar_xdr::curr::SorobanTransactionMetaExt::V1(v1) => { + non_refundable_fee = v1.total_non_refundable_resource_fee_charged; + refundable_fee = v1.total_refundable_resource_fee_charged; + rent_fee = v1.rent_fee_charged; + has_soroban_meta = true; } } } - _ => {} } } } @@ -166,10 +164,10 @@ mod tests { use super::*; use crate::xdr::codec::XdrCodec; use stellar_xdr::curr::{ - Memo, MuxedAccount, Preconditions, SequenceNumber, Transaction, TransactionEnvelope, - TransactionExt, TransactionResult, TransactionResultResult, TransactionV1Envelope, Uint256, - TransactionMeta, TransactionMetaV3, SorobanTransactionMeta, SorobanTransactionMetaExt, - SorobanTransactionMetaExtV1, ExtensionPoint, + ExtensionPoint, Memo, MuxedAccount, Preconditions, SequenceNumber, SorobanTransactionMeta, + SorobanTransactionMetaExt, SorobanTransactionMetaExtV1, Transaction, TransactionEnvelope, + TransactionExt, TransactionMeta, TransactionMetaV3, TransactionResult, + TransactionResultResult, TransactionV1Envelope, Uint256, }; #[test] diff --git a/crates/core/src/decode/contract_error.rs b/crates/core/src/decode/contract_error.rs index 91f1d065..3655efff 100644 --- a/crates/core/src/decode/contract_error.rs +++ b/crates/core/src/decode/contract_error.rs @@ -1,6 +1,3 @@ - - -use crate::decode::decode_context::DecodeContext; use crate::error::{PrismError, PrismResult}; use crate::spec::decoder; use crate::types::address::Address; @@ -10,7 +7,7 @@ use crate::types::report::ContractErrorInfo; pub async fn resolve( contract_id: &str, error_code: u32, - ctx: &DecodeContext, + ctx: &crate::decode::decode_context::DecodeContext, ) -> PrismResult { resolve_with_network(contract_id, error_code, &ctx.network).await } diff --git a/crates/core/src/decode/cross_contract.rs b/crates/core/src/decode/cross_contract.rs index 8b1690cf..22cbc5d1 100644 --- a/crates/core/src/decode/cross_contract.rs +++ b/crates/core/src/decode/cross_contract.rs @@ -4,9 +4,7 @@ //! chain that emitted a failure event, attributing the error to that contract //! rather than the top-level invoker. -use stellar_xdr::curr::{ - ContractEventBody, ContractEventType, DiagnosticEvent, Hash, ScVal, -}; +use stellar_xdr::curr::{ContractEventBody, ContractEventType, DiagnosticEvent, Hash, ScVal}; use crate::error::PrismResult; use crate::types::report::{DiagnosticReport, FailureAttribution}; @@ -73,9 +71,7 @@ fn process_event( ) { // Only care about system-emitted diagnostic events (in_successful_contract_call == false // means the surrounding call failed, but we want the frame itself). - let v0 = match &event.event.body { - ContractEventBody::V0(v) => v, - }; + let ContractEventBody::V0(v0) = &event.event.body; let contract_address = match &event.event.contract_id { Some(hash) => hash_to_string(hash), @@ -83,7 +79,7 @@ fn process_event( }; let topics: Vec = v0.topics.iter().filter_map(scval_to_string).collect(); - let first_topic = topics.first().map(|s| s.as_str()).unwrap_or(""); + let first_topic = topics.first().map_or("", std::string::String::as_str); match first_topic { // fn_call / fn_return are emitted by the host for every cross-contract @@ -117,8 +113,7 @@ fn process_event( _ => { // For non-system event types that arrive while in_successful_contract_call // is false we treat the emitting contract as the failure origin. - if event.event.type_ == ContractEventType::System - && !event.in_successful_contract_call + if event.event.type_ == ContractEventType::System && !event.in_successful_contract_call { let frame = call_stack.last().cloned().unwrap_or(CallFrame { contract_address, diff --git a/crates/core/src/decode/decode_context.rs b/crates/core/src/decode/decode_context.rs index 7bf10d43..d61ad2ad 100644 --- a/crates/core/src/decode/decode_context.rs +++ b/crates/core/src/decode/decode_context.rs @@ -1,4 +1,4 @@ -use crate::network::config::{Network, NetworkConfig}; +use crate::types::config::NetworkConfig; /// Output format for diagnostic reports. #[derive(Debug, Clone, PartialEq, Eq, Default)] @@ -89,6 +89,7 @@ impl From<&NetworkConfig> for DecodeContextBuilder { #[cfg(test)] mod tests { use super::*; + use crate::network::config::Network; #[test] fn builder_defaults_to_testnet() { diff --git a/crates/core/src/decode/diagnostic.rs b/crates/core/src/decode/diagnostic.rs index cd0ecce1..d64f14c5 100644 --- a/crates/core/src/decode/diagnostic.rs +++ b/crates/core/src/decode/diagnostic.rs @@ -48,11 +48,11 @@ fn scval_to_string(val: &ScVal) -> Option { ScVal::Void => Some("Void".to_string()), ScVal::Bool(b) => Some(b.to_string()), ScVal::U128(u) => { - let num = ((u.hi as u128) << 64) | (u.lo as u128); + let num = (u128::from(u.hi) << 64) | u128::from(u.lo); Some(num.to_string()) } ScVal::I128(i) => { - let num = ((i.hi as i128) << 64) | (i.lo as u128 as i128); + let num = (i128::from(i.hi) << 64) | (u128::from(i.lo) as i128); Some(num.to_string()) } ScVal::Vec(Some(v)) => { @@ -68,7 +68,7 @@ fn scval_to_string(val: &ScVal) -> Option { .map(|entry| { let k = scval_to_string(&entry.key).unwrap_or_else(|| "?".to_string()); let v = scval_to_string(&entry.val).unwrap_or_else(|| "?".to_string()); - format!("{}: {}", k, v) + format!("{k}: {v}") }) .collect(); Some(format!("{{{}}}", items.join(", "))) @@ -149,7 +149,7 @@ fn deepest_error_event(events: &[DiagnosticEvent]) -> Option= current.depth }); @@ -314,11 +314,11 @@ fn analyze_diagnostic_event(report: &mut DiagnosticReport, event: &DiagnosticEve if !report.detailed_explanation.contains(&topics_str) { if report.detailed_explanation.is_empty() { report.detailed_explanation = - format!("Diagnostic events trace:\n- [{}]", topics_str); + format!("Diagnostic events trace:\n- [{topics_str}]"); } else { report .detailed_explanation - .push_str(&format!("\n- [{}]", topics_str)); + .push_str(&format!("\n- [{topics_str}]")); } } } diff --git a/crates/core/src/decode/host_error.rs b/crates/core/src/decode/host_error.rs index 5705a156..86eb5923 100644 --- a/crates/core/src/decode/host_error.rs +++ b/crates/core/src/decode/host_error.rs @@ -1,5 +1,3 @@ - - use serde::Serialize; use stellar_xdr::curr::{ InvokeHostFunctionResult, OperationResult, OperationResultTr, TransactionResult, @@ -13,27 +11,49 @@ use crate::xdr::codec::XdrCodec; #[derive(Debug, Clone, PartialEq, Serialize)] #[serde(tag = "category", rename_all = "snake_case")] pub enum HostError { - Budget { code: u32 }, - Storage { code: u32 }, - Auth { code: u32 }, - Context { code: u32 }, - Value { code: u32 }, - Object { code: u32 }, - Crypto { code: u32 }, - Contract { code: u32 }, - Wasm { code: u32 }, - Events { code: u32 }, + Budget { + code: u32, + }, + Storage { + code: u32, + }, + Auth { + code: u32, + }, + Context { + code: u32, + }, + Value { + code: u32, + }, + Object { + code: u32, + }, + Crypto { + code: u32, + }, + Contract { + code: u32, + }, + Wasm { + code: u32, + }, + Events { + code: u32, + }, ContractSpecific { contract_id: Option, code: u32, }, - Unknown { type_code: u32, sub_code: u32 }, + Unknown { + type_code: u32, + sub_code: u32, + }, } impl HostError { - pub fn category_name(&self) -> &str { match self { Self::Budget { .. } => "Budget", @@ -161,15 +181,11 @@ pub fn from_transaction_result(tx_result: TransactionResult) -> PrismResult return Err(PrismError::TransactionSucceeded), - InvokeHostFunctionResult::Trapped => { - - (ErrorCategory::Contract, 0u32, false) - } + InvokeHostFunctionResult::Trapped => (ErrorCategory::Contract, 0u32, false), InvokeHostFunctionResult::ResourceLimitExceeded => (ErrorCategory::Budget, 0, false), InvokeHostFunctionResult::EntryArchived => (ErrorCategory::Storage, 0, false), - InvokeHostFunctionResult::Malformed | InvokeHostFunctionResult::InsufficientRefundableFee => { - (ErrorCategory::Context, 0, false) - } + InvokeHostFunctionResult::Malformed + | InvokeHostFunctionResult::InsufficientRefundableFee => (ErrorCategory::Context, 0, false), }; Ok(ClassifiedError { @@ -259,11 +275,19 @@ mod tests { assert_eq!(HostError::Wasm { code: 0 }.category_name(), "Wasm"); assert_eq!(HostError::Events { code: 0 }.category_name(), "Events"); assert_eq!( - HostError::ContractSpecific { contract_id: None, code: 42 }.category_name(), + HostError::ContractSpecific { + contract_id: None, + code: 42 + } + .category_name(), "ContractSpecific" ); assert_eq!( - HostError::Unknown { type_code: 99, sub_code: 1 }.category_name(), + HostError::Unknown { + type_code: 99, + sub_code: 1 + } + .category_name(), "Unknown" ); } @@ -282,7 +306,10 @@ mod tests { #[test] fn test_unknown_variant() { - let err = HostError::Unknown { type_code: 7, sub_code: 255 }; + let err = HostError::Unknown { + type_code: 7, + sub_code: 255, + }; let json = serde_json::to_value(&err).unwrap(); assert_eq!(json["category"], "unknown"); assert_eq!(json["type_code"], 7); @@ -292,7 +319,10 @@ mod tests { #[test] fn test_parse_error_category() { assert_eq!(parse_error_category("budget"), Some(ErrorCategory::Budget)); - assert_eq!(parse_error_category("STORAGE"), Some(ErrorCategory::Storage)); + assert_eq!( + parse_error_category("STORAGE"), + Some(ErrorCategory::Storage) + ); assert_eq!(parse_error_category("unknown_xyz"), None); } @@ -369,7 +399,11 @@ mod tests { #[test] fn test_summary_unknown_variant() { - let s = HostError::Unknown { type_code: 9, sub_code: 42 }.summary(); + let s = HostError::Unknown { + type_code: 9, + sub_code: 42, + } + .summary(); assert!(s.contains("9")); assert!(s.contains("42")); assert!(s.contains("not recognised")); @@ -377,7 +411,6 @@ mod tests { #[test] fn test_summary_unknown_codes_fallback() { - let s = HostError::Budget { code: 99 }.summary(); assert!(s.contains("99")); assert!(s.contains("Budget") || s.contains("budget")); @@ -385,7 +418,6 @@ mod tests { #[test] fn test_summary_under_120_chars() { - let errors = vec![ HostError::Budget { code: 0 }, HostError::Storage { code: 0 }, diff --git a/crates/core/src/decode/mappings/auth.rs b/crates/core/src/decode/mappings/auth.rs index 62b621bc..fab62582 100644 --- a/crates/core/src/decode/mappings/auth.rs +++ b/crates/core/src/decode/mappings/auth.rs @@ -1,10 +1,7 @@ - - use crate::types::report::Severity; #[derive(Debug, Clone, PartialEq, Eq)] pub struct AuthErrorDetail { - pub code: u32, pub name: &'static str, diff --git a/crates/core/src/decode/mappings/budget.rs b/crates/core/src/decode/mappings/budget.rs index d010fd27..8a554b20 100644 --- a/crates/core/src/decode/mappings/budget.rs +++ b/crates/core/src/decode/mappings/budget.rs @@ -1,5 +1,3 @@ - - use crate::types::report::Severity; #[derive(Debug, Clone, PartialEq, Eq)] @@ -23,7 +21,6 @@ impl From for Severity { #[derive(Debug, Clone, PartialEq, Eq)] pub struct BudgetErrorDetail { - pub code: u32, pub name: &'static str, @@ -61,7 +58,9 @@ pub const BUDGET_ERROR_DETAILS: &[BudgetErrorDetail] = &[ ]; pub fn lookup(code: u32) -> Option<&'static BudgetErrorDetail> { - BUDGET_ERROR_DETAILS.iter().find(|detail| detail.code == code) + BUDGET_ERROR_DETAILS + .iter() + .find(|detail| detail.code == code) } #[cfg(test)] diff --git a/crates/core/src/decode/mappings/context.rs b/crates/core/src/decode/mappings/context.rs index 33100c65..f878481b 100644 --- a/crates/core/src/decode/mappings/context.rs +++ b/crates/core/src/decode/mappings/context.rs @@ -1,5 +1,3 @@ - - use crate::types::report::Severity; #[derive(Debug, Clone, PartialEq, Eq)] diff --git a/crates/core/src/decode/mappings/mod.rs b/crates/core/src/decode/mappings/mod.rs index 6fcce64e..c8af08b7 100644 --- a/crates/core/src/decode/mappings/mod.rs +++ b/crates/core/src/decode/mappings/mod.rs @@ -1,5 +1,3 @@ - - pub mod auth; pub mod budget; pub mod context; diff --git a/crates/core/src/decode/mappings/object.rs b/crates/core/src/decode/mappings/object.rs index d93bb837..62ef10e3 100644 --- a/crates/core/src/decode/mappings/object.rs +++ b/crates/core/src/decode/mappings/object.rs @@ -55,7 +55,9 @@ pub const OBJECT_ERROR_DETAILS: &[ObjectErrorDetail] = &[ ]; pub fn lookup(code: u32) -> Option<&'static ObjectErrorDetail> { - OBJECT_ERROR_DETAILS.iter().find(|detail| detail.code == code) + OBJECT_ERROR_DETAILS + .iter() + .find(|detail| detail.code == code) } #[cfg(test)] @@ -66,7 +68,10 @@ mod tests { fn lookup_returns_vec_index_out_of_bound_detail() { let detail = lookup(5).expect("vec index out of bound detail"); assert_eq!(detail.name, "VecIndexOutOfBound"); - assert!(detail.summary.contains("index out of bounds") || detail.summary.contains("index out of bounds")); + assert!( + detail.summary.contains("index out of bounds") + || detail.summary.contains("index out of bounds") + ); } #[test] diff --git a/crates/core/src/decode/mappings/severity_tests.rs b/crates/core/src/decode/mappings/severity_tests.rs index 5844c64f..7359f32b 100644 --- a/crates/core/src/decode/mappings/severity_tests.rs +++ b/crates/core/src/decode/mappings/severity_tests.rs @@ -215,7 +215,10 @@ mod tests { for entry in BUDGET_ERROR_DETAILS { let sev: Severity = entry.severity.clone().into(); assert!( - matches!(sev, Severity::Fatal | Severity::Error | Severity::Warning | Severity::Info), + matches!( + sev, + Severity::Fatal | Severity::Error | Severity::Warning | Severity::Info + ), "Unexpected severity for budget code {}: {:?}", entry.code, sev @@ -230,7 +233,10 @@ mod tests { for entry in VALUE_ERROR_DETAILS { let sev: Severity = entry.severity.clone().into(); assert!( - matches!(sev, Severity::Fatal | Severity::Error | Severity::Warning | Severity::Info), + matches!( + sev, + Severity::Fatal | Severity::Error | Severity::Warning | Severity::Info + ), "Unexpected severity for value code {}: {:?}", entry.code, sev @@ -244,7 +250,10 @@ mod tests { for entry in STORAGE_ERROR_DETAILS { assert!( - matches!(entry.severity, Severity::Fatal | Severity::Error | Severity::Warning | Severity::Info), + matches!( + entry.severity, + Severity::Fatal | Severity::Error | Severity::Warning | Severity::Info + ), "Unexpected severity for storage code {}: {:?}", entry.code, entry.severity @@ -258,7 +267,10 @@ mod tests { for entry in CONTEXT_ERROR_DETAILS { assert!( - matches!(entry.severity, Severity::Fatal | Severity::Error | Severity::Warning | Severity::Info), + matches!( + entry.severity, + Severity::Fatal | Severity::Error | Severity::Warning | Severity::Info + ), "Unexpected severity for context code {}: {:?}", entry.code, entry.severity @@ -272,7 +284,10 @@ mod tests { for entry in AUTH_ERROR_DETAILS { assert!( - matches!(entry.severity, Severity::Fatal | Severity::Error | Severity::Warning | Severity::Info), + matches!( + entry.severity, + Severity::Fatal | Severity::Error | Severity::Warning | Severity::Info + ), "Unexpected severity for auth code {}: {:?}", entry.code, entry.severity diff --git a/crates/core/src/decode/mappings/storage.rs b/crates/core/src/decode/mappings/storage.rs index 3d673281..81a45fe8 100644 --- a/crates/core/src/decode/mappings/storage.rs +++ b/crates/core/src/decode/mappings/storage.rs @@ -1,10 +1,7 @@ - - use crate::types::report::Severity; #[derive(Debug, Clone, PartialEq, Eq)] pub struct StorageErrorDetail { - pub code: u32, pub name: &'static str, @@ -48,7 +45,9 @@ pub const STORAGE_ERROR_DETAILS: &[StorageErrorDetail] = &[ ]; pub fn lookup(code: u32) -> Option<&'static StorageErrorDetail> { - STORAGE_ERROR_DETAILS.iter().find(|detail| detail.code == code) + STORAGE_ERROR_DETAILS + .iter() + .find(|detail| detail.code == code) } #[cfg(test)] diff --git a/crates/core/src/decode/mappings/value.rs b/crates/core/src/decode/mappings/value.rs index 674d6a6c..1ceae74b 100644 --- a/crates/core/src/decode/mappings/value.rs +++ b/crates/core/src/decode/mappings/value.rs @@ -1,5 +1,3 @@ - - use crate::types::report::Severity; #[derive(Debug, Clone, PartialEq, Eq)] @@ -23,7 +21,6 @@ impl From for Severity { #[derive(Debug, Clone, PartialEq, Eq)] pub struct ValueErrorDetail { - pub code: u32, pub name: &'static str, @@ -85,7 +82,9 @@ pub const VALUE_ERROR_DETAILS: &[ValueErrorDetail] = &[ ]; pub fn lookup(code: u32) -> Option<&'static ValueErrorDetail> { - VALUE_ERROR_DETAILS.iter().find(|detail| detail.code == code) + VALUE_ERROR_DETAILS + .iter() + .find(|detail| detail.code == code) } #[cfg(test)] diff --git a/crates/core/src/decode/mod.rs b/crates/core/src/decode/mod.rs index 130fec37..273290ce 100644 --- a/crates/core/src/decode/mod.rs +++ b/crates/core/src/decode/mod.rs @@ -1,27 +1,27 @@ - pub mod auth; pub mod auth_signature; pub mod context; pub mod contract_error; pub mod cross_contract; +pub mod decode_context; pub mod diagnostic; pub mod host_error; pub mod mappings; pub mod report; pub mod walker; -pub use auth::{ - AddressCredential, AuthChain, AuthCredential, AuthFunctionKind, AuthInvocation, -}; +pub use auth::{AddressCredential, AuthChain, AuthCredential, AuthFunctionKind, AuthInvocation}; pub use walker::{ - walk_diagnostic_events, DiagnosticEventKind, DiagnosticEventWalker, - StructuredDiagnosticEvent, + walk_diagnostic_events, DiagnosticEventKind, DiagnosticEventWalker, StructuredDiagnosticEvent, }; use crate::error::{PrismError, PrismResult}; use crate::types::report::DiagnosticReport; use crate::xdr::codec::XdrCodec; -use stellar_xdr::curr::{ScVal, SorobanTransactionMetaExt, TransactionMeta, TransactionResult, TransactionEnvelope, FeeBumpTransactionInnerTx}; +use stellar_xdr::curr::{ + ScVal, SorobanTransactionMetaExt, + TransactionMeta, TransactionResult, +}; /// Decode `resultMetaXdr` as `TransactionMeta` and, if it is V3, inject the /// Soroban contract events, diagnostic events, and return value into the JSON @@ -41,12 +41,11 @@ fn parse_v3_metadata(tx_data: &mut serde_json::Value) -> PrismResult<()> { None => return Ok(()), }; - let meta = TransactionMeta::from_xdr_base64(&meta_b64).map_err(|e| { - PrismError::XdrDecodingFailed { + let meta = + TransactionMeta::from_xdr_base64(&meta_b64).map_err(|e| PrismError::XdrDecodingFailed { type_name: "TransactionMeta", reason: e.to_string(), - } - })?; + })?; if let TransactionMeta::V3(v3) = meta { let soroban_meta = match v3.soroban_meta { @@ -148,10 +147,12 @@ pub async fn decode_transaction_with_op_filter( parse_v3_metadata(&mut base_tx_data)?; // Decode the envelope XDR to determine the number of operations in the transaction. - let num_ops = if let Some(envelope_str) = base_tx_data.get("envelopeXdr").and_then(|v| v.as_str()) { + let num_ops = if let Some(envelope_str) = + base_tx_data.get("envelopeXdr").and_then(|v| v.as_str()) + { // Use the XDR codec to parse the envelope. let envelope = ::from_xdr_base64(envelope_str) - .map_err(|e| crate::error::PrismError::Internal(format!("Failed to decode envelope XDR: {}", e)))?; + .map_err(|e| crate::error::PrismError::Internal(format!("Failed to decode envelope XDR: {e}")))?; match envelope { stellar_xdr::curr::TransactionEnvelope::TxV0(v0) => v0.tx.operations.len(), stellar_xdr::curr::TransactionEnvelope::Tx(v1) => v1.tx.operations.len(), @@ -181,10 +182,11 @@ pub async fn decode_transaction_with_op_filter( let mut report = report::build_report(&error_info)?; if error_info.is_contract_error { + let ctx = crate::decode::decode_context::DecodeContextBuilder::from(network).build(); if let Ok(contract_info) = contract_error::resolve( &error_info.contract_id.unwrap_or_default(), error_info.error_code, - network, + &ctx, ) .await { diff --git a/crates/core/src/decode/walker.rs b/crates/core/src/decode/walker.rs index 554296a3..b7e11798 100644 --- a/crates/core/src/decode/walker.rs +++ b/crates/core/src/decode/walker.rs @@ -32,9 +32,7 @@ use serde::{Deserialize, Serialize}; use stellar_strkey::Contract as StrkeyContract; -use stellar_xdr::curr::{ - ContractEventBody, ContractEventType, DiagnosticEvent, Hash, ScVal, -}; +use stellar_xdr::curr::{ContractEventBody, ContractEventType, DiagnosticEvent, Hash, ScVal}; // --------------------------------------------------------------------------- // Public types @@ -196,7 +194,7 @@ impl DiagnosticEventWalker { let contract_id = inner .contract_id .as_ref() - .map(|hash| Self::hash_to_strkey(hash)); + .map(Self::hash_to_strkey); // Extract topics and data from the event body. match &inner.body { @@ -235,9 +233,7 @@ impl Default for DiagnosticEventWalker { /// /// Convenience wrapper; prefer constructing the walker explicitly when you /// need to call it multiple times in a hot path. -pub fn walk_diagnostic_events( - events: &[DiagnosticEvent], -) -> Vec { +pub fn walk_diagnostic_events(events: &[DiagnosticEvent]) -> Vec { DiagnosticEventWalker::new().walk(events.iter()) } @@ -377,7 +373,10 @@ mod tests { true, ); let result = DiagnosticEventWalker::new().walk(std::iter::once(&event)); - assert_eq!(result[0].contract_id.as_deref(), Some(expected_strkey.as_str())); + assert_eq!( + result[0].contract_id.as_deref(), + Some(expected_strkey.as_str()) + ); // Stellar contract strkeys start with 'C' assert!(result[0].contract_id.as_ref().unwrap().starts_with('C')); } @@ -503,13 +502,7 @@ mod tests { #[test] fn event_with_empty_topics_is_accepted() { - let event = make_event( - ContractEventType::System, - None, - vec![], - ScVal::Void, - true, - ); + let event = make_event(ContractEventType::System, None, vec![], ScVal::Void, true); let result = DiagnosticEventWalker::new().walk(std::iter::once(&event)); assert_eq!(result[0].topics.len(), 0); assert_eq!(result[0].data, ScVal::Void); @@ -529,21 +522,55 @@ mod tests { 1 => ContractEventType::System, _ => ContractEventType::Diagnostic, }; - make_event(kind, Some(contract_hash(i as u8)), vec![sym("topic")], ScVal::Void, i % 2 == 0) + make_event( + kind, + Some(contract_hash(i as u8)), + vec![sym("topic")], + ScVal::Void, + i % 2 == 0, + ) }) .collect(); let result = DiagnosticEventWalker::new().walk(events.iter()); - assert_eq!(result.len(), n, "output count must match input count exactly"); + assert_eq!( + result.len(), + n, + "output count must match input count exactly" + ); } #[test] fn output_count_equals_input_count_for_mixed_batch() { let events = vec![ - make_event(ContractEventType::Contract, Some(contract_hash(1)), vec![sym("a")], u32_val(1), true), - make_event(ContractEventType::System, None, vec![sym("b")], ScVal::Void, true), - make_event(ContractEventType::Diagnostic, None, vec![sym("c"), sym("d")], ScVal::Bool(false), false), - make_event(ContractEventType::Contract, Some(contract_hash(2)), vec![], ScVal::Void, true), + make_event( + ContractEventType::Contract, + Some(contract_hash(1)), + vec![sym("a")], + u32_val(1), + true, + ), + make_event( + ContractEventType::System, + None, + vec![sym("b")], + ScVal::Void, + true, + ), + make_event( + ContractEventType::Diagnostic, + None, + vec![sym("c"), sym("d")], + ScVal::Bool(false), + false, + ), + make_event( + ContractEventType::Contract, + Some(contract_hash(2)), + vec![], + ScVal::Void, + true, + ), ]; let result = DiagnosticEventWalker::new().walk(events.iter()); @@ -560,8 +587,20 @@ mod tests { #[test] fn walk_diagnostic_events_convenience_fn_matches_walker_output() { let events = vec![ - make_event(ContractEventType::Contract, Some(contract_hash(10)), vec![sym("transfer")], u32_val(99), true), - make_event(ContractEventType::System, None, vec![sym("host_fn")], ScVal::Void, true), + make_event( + ContractEventType::Contract, + Some(contract_hash(10)), + vec![sym("transfer")], + u32_val(99), + true, + ), + make_event( + ContractEventType::System, + None, + vec![sym("host_fn")], + ScVal::Void, + true, + ), ]; let via_fn = walk_diagnostic_events(&events); let via_walker = DiagnosticEventWalker::new().walk(events.iter()); @@ -583,9 +622,27 @@ mod tests { #[test] fn output_ordering_mirrors_input_ordering() { let events = vec![ - make_event(ContractEventType::Contract, Some(contract_hash(10)), vec![sym("first")], u32_val(1), true), - make_event(ContractEventType::System, None, vec![sym("second")], u32_val(2), true), - make_event(ContractEventType::Diagnostic, None, vec![sym("third")], u32_val(3), false), + make_event( + ContractEventType::Contract, + Some(contract_hash(10)), + vec![sym("first")], + u32_val(1), + true, + ), + make_event( + ContractEventType::System, + None, + vec![sym("second")], + u32_val(2), + true, + ), + make_event( + ContractEventType::Diagnostic, + None, + vec![sym("third")], + u32_val(3), + false, + ), ]; let result = DiagnosticEventWalker::new().walk(events.iter()); @@ -601,9 +658,21 @@ mod tests { #[test] fn all_kind_variants_are_reachable_from_xdr_type() { - let contract_event = make_event(ContractEventType::Contract, Some(contract_hash(20)), vec![], ScVal::Void, true); + let contract_event = make_event( + ContractEventType::Contract, + Some(contract_hash(20)), + vec![], + ScVal::Void, + true, + ); let system_event = make_event(ContractEventType::System, None, vec![], ScVal::Void, true); - let debug_event = make_event(ContractEventType::Diagnostic, None, vec![], ScVal::Void, true); + let debug_event = make_event( + ContractEventType::Diagnostic, + None, + vec![], + ScVal::Void, + true, + ); let events = vec![contract_event, system_event, debug_event]; let result = DiagnosticEventWalker::new().walk(events.iter()); @@ -635,11 +704,7 @@ mod tests { #[test] fn multiple_topics_of_mixed_types_survive_round_trip() { - let topics = vec![ - sym("transfer"), - ScVal::U32(1_234_567), - ScVal::Bool(true), - ]; + let topics = vec![sym("transfer"), ScVal::U32(1_234_567), ScVal::Bool(true)]; let event = make_event( ContractEventType::Contract, Some(contract_hash(31)), @@ -660,7 +725,13 @@ mod tests { #[test] fn default_walker_behaves_identically_to_new() { - let event = make_event(ContractEventType::Contract, Some(contract_hash(99)), vec![sym("x")], ScVal::Void, true); + let event = make_event( + ContractEventType::Contract, + Some(contract_hash(99)), + vec![sym("x")], + ScVal::Void, + true, + ); let a = DiagnosticEventWalker::new().walk(std::iter::once(&event)); let b = DiagnosticEventWalker::default().walk(std::iter::once(&event)); assert_eq!(a[0].kind, b[0].kind); diff --git a/crates/core/src/error.rs b/crates/core/src/error.rs index 44d6dcd8..4ad6c795 100644 --- a/crates/core/src/error.rs +++ b/crates/core/src/error.rs @@ -1,12 +1,9 @@ - - use serde::{Deserialize, Serialize}; use thiserror::Error; #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Error)] #[error("JSON-RPC error (code: {code}): {message}")] pub struct JsonRpcError { - pub code: i64, pub message: String, @@ -14,7 +11,6 @@ pub struct JsonRpcError { #[derive(Debug, Error)] pub enum ArchiveErrorKind { - #[error("checksum mismatch for '{file}': expected {expected}, got {actual}")] ChecksumMismatch { file: String, @@ -34,7 +30,6 @@ pub enum ArchiveErrorKind { #[derive(Debug, Error)] pub enum PrismError { - #[error("RPC request timed out after {timeout_secs}s (method: {method})")] NetworkTimeout { method: String, timeout_secs: u64 }, diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index cd5b73ea..4e980a11 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -1,5 +1,3 @@ - - pub mod archive; pub mod cache; pub mod debugger; @@ -13,15 +11,15 @@ pub mod taxonomy; pub mod types; pub mod xdr; -pub use network::config::Network; -pub use types::address::Address; -pub use types::config::NetworkConfig; -pub use error::{PrismError, PrismResult}; -pub use types::report::DiagnosticReport; pub use decode::{ walk_diagnostic_events, AddressCredential, AuthChain, AuthCredential, AuthFunctionKind, AuthInvocation, DiagnosticEventKind, DiagnosticEventWalker, StructuredDiagnosticEvent, }; +pub use error::{PrismError, PrismResult}; +pub use network::config::Network; +pub use types::address::Address; +pub use types::config::NetworkConfig; +pub use types::report::DiagnosticReport; pub const VERSION: &str = env!("CARGO_PKG_VERSION"); diff --git a/crates/core/src/network/config.rs b/crates/core/src/network/config.rs index 87f96cc6..e905f9c4 100644 --- a/crates/core/src/network/config.rs +++ b/crates/core/src/network/config.rs @@ -1,7 +1,5 @@ - - use crate::error::{PrismError, PrismResult}; -use crate::rpc::jsonrpc::{JsonRpcTransport, JsonRpcRequest, GetHealthParams}; +use crate::rpc::jsonrpc::{GetHealthParams, JsonRpcRequest, JsonRpcTransport}; use serde::{Deserialize, Serialize}; use std::fmt; use std::str::FromStr; @@ -21,8 +19,7 @@ const TESTNET_ARCHIVE_URLS: [&str; 1] = ["https://history.stellar.org/prd/core-testnet/core_testnet_001"]; const FUTURENET_ARCHIVE_URLS: [&str; 1] = ["https://history-futurenet.stellar.org"]; -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -#[derive(Default)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)] pub enum Network { Mainnet, #[default] @@ -32,7 +29,6 @@ pub enum Network { } impl Network { - pub const LOCAL: &str = "local"; pub fn parse(value: &str) -> PrismResult { @@ -129,7 +125,6 @@ impl<'de> Deserialize<'de> for Network { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct NetworkConfig { - pub network: Network, pub rpc_url: String, @@ -144,7 +139,6 @@ pub struct NetworkConfig { } impl NetworkConfig { - pub fn testnet() -> Self { Self { network: Network::Testnet, @@ -256,10 +250,7 @@ pub fn default_network() -> NetworkConfig { pub async fn validate_network(config: &NetworkConfig) -> bool { let transport = JsonRpcTransport::new(&config.rpc_url, 0); let req = JsonRpcRequest::new(1, "getHealth", GetHealthParams {}); - transport - .call::<_, serde_json::Value>(&req) - .await - .is_ok() + transport.call::<_, serde_json::Value>(&req).await.is_ok() } #[cfg(test)] diff --git a/crates/core/src/network/mod.rs b/crates/core/src/network/mod.rs index 694c8f1c..9672f56a 100644 --- a/crates/core/src/network/mod.rs +++ b/crates/core/src/network/mod.rs @@ -1,5 +1,3 @@ - - pub mod config; pub use config::{Network, NetworkConfig}; diff --git a/crates/core/src/replay/differ.rs b/crates/core/src/replay/differ.rs index d8c56b2e..75085f82 100644 --- a/crates/core/src/replay/differ.rs +++ b/crates/core/src/replay/differ.rs @@ -1,8 +1,6 @@ - - +use crate::error::PrismResult; use crate::replay::sandbox::SandboxResult; use crate::replay::state::LedgerState; -use crate::error::PrismResult; use crate::types::trace::{DiffChangeType, LedgerEntryDiff, StateDiff}; pub fn compute_diff(pre_state: &LedgerState, result: &SandboxResult) -> PrismResult { diff --git a/crates/core/src/replay/mod.rs b/crates/core/src/replay/mod.rs index 1afbca73..101f3e38 100644 --- a/crates/core/src/replay/mod.rs +++ b/crates/core/src/replay/mod.rs @@ -1,13 +1,11 @@ - - pub mod differ; pub mod profiler; pub mod sandbox; pub mod state; pub mod trace; -use crate::types::config::NetworkConfig; use crate::error::PrismResult; +use crate::types::config::NetworkConfig; use crate::types::trace::ExecutionTrace; pub async fn replay_transaction( diff --git a/crates/core/src/replay/profiler.rs b/crates/core/src/replay/profiler.rs index f05f1d9f..dfa9f5b9 100644 --- a/crates/core/src/replay/profiler.rs +++ b/crates/core/src/replay/profiler.rs @@ -1,13 +1,11 @@ - - -use crate::replay::sandbox::SandboxResult; use crate::error::PrismResult; +use crate::replay::sandbox::SandboxResult; use crate::types::trace::ResourceProfile; pub fn generate_profile(result: &SandboxResult) -> PrismResult { let mut profile = ResourceProfile { total_cpu: result.total_cpu, - cpu_limit: 0, + cpu_limit: 0, total_memory: result.total_memory, memory_limit: 0, total_read_bytes: 0, diff --git a/crates/core/src/replay/sandbox.rs b/crates/core/src/replay/sandbox.rs index dc7aa987..8809a5dc 100644 --- a/crates/core/src/replay/sandbox.rs +++ b/crates/core/src/replay/sandbox.rs @@ -1,11 +1,8 @@ - - -use crate::replay::state::LedgerState; use crate::error::{PrismError, PrismResult}; +use crate::replay::state::LedgerState; #[derive(Debug, Clone, serde::Serialize)] pub struct TraceEvent { - pub event_type: TraceEventType, pub timestamp_us: u64, @@ -15,7 +12,6 @@ pub struct TraceEvent { #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)] pub enum TraceEventType { - InvocationStart, InvocationEnd, @@ -37,7 +33,6 @@ pub enum TraceEventType { #[derive(Debug)] pub struct SandboxResult { - pub success: bool, pub events: Vec, @@ -53,7 +48,6 @@ pub async fn execute_with_tracing( _state: &LedgerState, _tx_hash: &str, ) -> PrismResult { - tracing::info!("Sandbox execution with tracing — not yet implemented"); Err(PrismError::ReplayError( diff --git a/crates/core/src/replay/state.rs b/crates/core/src/replay/state.rs index b93bfb05..906f0278 100644 --- a/crates/core/src/replay/state.rs +++ b/crates/core/src/replay/state.rs @@ -1,12 +1,9 @@ - - -use crate::types::config::NetworkConfig; use crate::error::{PrismError, PrismResult}; +use crate::types::config::NetworkConfig; use std::collections::HashMap; #[derive(Debug, Clone)] pub struct LedgerState { - pub ledger_sequence: u32, pub entries: HashMap>, @@ -16,7 +13,6 @@ pub struct LedgerState { #[derive(Debug, Clone, PartialEq, Eq)] pub enum ReconstructionPath { - HotPath, ColdPath, @@ -28,9 +24,9 @@ pub async fn reconstruct_state(tx_hash: &str, network: &NetworkConfig) -> PrismR let rpc = crate::rpc::SorobanRpcClient::new(network); let tx_data = rpc.get_transaction(tx_hash).await?; - let tx_ledger = tx_data - .ledger - .ok_or_else(|| PrismError::ReplayError("Cannot determine transaction ledger".to_string()))?; + let tx_ledger = tx_data.ledger.ok_or_else(|| { + PrismError::ReplayError("Cannot determine transaction ledger".to_string()) + })?; let latest: serde_json::Value = rpc.get_latest_ledger().await?; let latest_ledger = latest diff --git a/crates/core/src/replay/trace.rs b/crates/core/src/replay/trace.rs index 7b3bb9cb..0bbf733d 100644 --- a/crates/core/src/replay/trace.rs +++ b/crates/core/src/replay/trace.rs @@ -1,7 +1,5 @@ - - -use crate::replay::sandbox::{SandboxResult, TraceEventType}; use crate::error::PrismResult; +use crate::replay::sandbox::{SandboxResult, TraceEventType}; use crate::types::trace::{ContractInvocation, HostFunctionCall}; pub fn build_trace_tree(result: &SandboxResult) -> PrismResult> { @@ -62,8 +60,7 @@ pub fn build_trace_tree(result: &SandboxResult) -> PrismResult { - } + _ => {} } } diff --git a/crates/core/src/rpc/client.rs b/crates/core/src/rpc/client.rs index 8e136830..7f6d5482 100644 --- a/crates/core/src/rpc/client.rs +++ b/crates/core/src/rpc/client.rs @@ -1,5 +1,3 @@ - - use crate::error::{PrismError, PrismResult}; use crate::network::NetworkConfig; use crate::rpc::jsonrpc::{JsonRpcRequest, JsonRpcResponse}; @@ -9,10 +7,9 @@ use std::time::{Duration, Instant}; const BASE_DELAY_MS: u64 = 100; -const MAX_DELAY_MS: u64 = 10_000; +const MAX_DELAY_MS: u64 = 10_000; fn backoff_duration(attempt: u32) -> Duration { - let ms = BASE_DELAY_MS.saturating_mul(2u64.saturating_pow(attempt)); Duration::from_millis(ms.min(MAX_DELAY_MS)) } @@ -85,7 +82,6 @@ impl SimulateTransactionResponse { #[derive(Debug, Clone)] pub struct SorobanRpcClient { - client: reqwest::Client, rpc_url: String, @@ -117,7 +113,6 @@ pub struct GetTransactionResponse { } impl SorobanRpcClient { - pub fn new(config: &NetworkConfig) -> Self { let mut headers = HeaderMap::new(); headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); @@ -238,9 +233,14 @@ impl SorobanRpcClient { // Retry on 429 Too Many Requests. if status == reqwest::StatusCode::TOO_MANY_REQUESTS { crate::rpc::record_rpc_duration(method, duration_secs, false); - tracing::warn!(method, attempt, "Rate limited by RPC node (429), will retry"); - last_error = - Some(PrismError::RpcError(format!("Rate limited (attempt {attempt})"))); + tracing::warn!( + method, + attempt, + "Rate limited by RPC node (429), will retry" + ); + last_error = Some(PrismError::RpcError(format!( + "Rate limited (attempt {attempt})" + ))); continue; } @@ -281,8 +281,8 @@ impl SorobanRpcClient { ))); } - let rpc_response: JsonRpcResponse = serde_json::from_str(&body) - .map_err(|e| { + let rpc_response: JsonRpcResponse = + serde_json::from_str(&body).map_err(|e| { crate::rpc::record_rpc_duration(method, duration_secs, false); PrismError::RpcError(format!("Response parse error: {e}")) })?; @@ -349,7 +349,10 @@ mod tests { /// connections than responses the last entry is repeated. /// Returns the bound local socket address. async fn spawn_mock_server(responses: Vec) -> std::net::SocketAddr { - use std::sync::{Arc, atomic::{AtomicUsize, Ordering}}; + use std::sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, + }; use tokio::io::AsyncReadExt; let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); @@ -359,7 +362,9 @@ mod tests { tokio::spawn(async move { loop { - let Ok((mut stream, _)) = listener.accept().await else { break }; + let Ok((mut stream, _)) = listener.accept().await else { + break; + }; let responses = Arc::clone(&responses); let counter = Arc::clone(&counter); tokio::spawn(async move { @@ -415,7 +420,6 @@ mod tests { #[test] fn backoff_is_capped_at_max_delay() { - assert_eq!(backoff_duration(7), Duration::from_millis(MAX_DELAY_MS)); assert_eq!(backoff_duration(63), Duration::from_millis(MAX_DELAY_MS)); @@ -609,7 +613,10 @@ mod tests { ]; let addr = spawn_mock_server(responses).await; let result = make_client(addr).get_latest_ledger().await; - assert!(result.is_ok(), "Expected success after retry, got: {result:?}"); + assert!( + result.is_ok(), + "Expected success after retry, got: {result:?}" + ); } #[tokio::test] @@ -639,7 +646,10 @@ mod tests { ]; let addr = spawn_mock_server(responses).await; let result = make_client(addr).get_latest_ledger().await; - assert!(result.is_ok(), "Expected success after retrying 503s, got: {result:?}"); + assert!( + result.is_ok(), + "Expected success after retrying 503s, got: {result:?}" + ); } #[tokio::test] @@ -650,7 +660,10 @@ mod tests { ]; let addr = spawn_mock_server(responses).await; let result = make_client(addr).get_latest_ledger().await; - assert!(result.is_ok(), "Expected success after retrying 502, got: {result:?}"); + assert!( + result.is_ok(), + "Expected success after retrying 502, got: {result:?}" + ); } #[tokio::test] @@ -661,12 +674,14 @@ mod tests { ]; let addr = spawn_mock_server(responses).await; let result = make_client(addr).get_latest_ledger().await; - assert!(result.is_ok(), "Expected success after retrying 429, got: {result:?}"); + assert!( + result.is_ok(), + "Expected success after retrying 429, got: {result:?}" + ); } #[tokio::test] async fn does_not_retry_on_4xx_client_error() { - let bad_body = r#"{"jsonrpc":"2.0","id":1,"error":{"code":-32600,"message":"Invalid request"}}"#; let responses = vec![http_response(400, "Bad Request", bad_body)]; @@ -678,8 +693,7 @@ mod tests { #[tokio::test] async fn returns_immediately_on_jsonrpc_error_in_200() { - let rpc_err = - r#"{"jsonrpc":"2.0","id":1,"error":{"code":-32000,"message":"not found"}}"#; + let rpc_err = r#"{"jsonrpc":"2.0","id":1,"error":{"code":-32000,"message":"not found"}}"#; let responses = vec![http_response(200, "OK", rpc_err)]; let addr = spawn_mock_server(responses).await; let result = make_client(addr).get_latest_ledger().await; @@ -717,7 +731,10 @@ mod tests { socket.write_all(response.as_bytes()).await.unwrap(); }); - let result = client.get_ledger_entries(&["key1".to_string()]).await.unwrap(); + let result = client + .get_ledger_entries(&["key1".to_string()]) + .await + .unwrap(); assert_eq!(result["entries"].as_array().unwrap().len(), 0); assert_eq!(result["latestLedger"], 123); } @@ -808,7 +825,8 @@ mod tests { tokio::spawn(async move { let (mut socket, _) = listener.accept().await.unwrap(); - let body = r#"{"jsonrpc":"2.0","id":1,"result":{"latestLedger":100,"error":"contract trap"}}"#; + let body = + r#"{"jsonrpc":"2.0","id":1,"result":{"latestLedger":100,"error":"contract trap"}}"#; let response = format!( "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\n\r\n{}", body.len(), diff --git a/crates/core/src/rpc/jsonrpc.rs b/crates/core/src/rpc/jsonrpc.rs index cc8b8138..20efa0a0 100644 --- a/crates/core/src/rpc/jsonrpc.rs +++ b/crates/core/src/rpc/jsonrpc.rs @@ -1,12 +1,10 @@ - - -use crate::error::{PrismError, PrismResult, JsonRpcError}; +use crate::error::{JsonRpcError, PrismError, PrismResult}; use serde::{Deserialize, Serialize}; use std::time::{Duration, Instant}; const BASE_DELAY_MS: u64 = 100; -const MAX_DELAY_MS: u64 = 10_000; +const MAX_DELAY_MS: u64 = 10_000; fn backoff_duration(attempt: u32) -> Duration { let ms = BASE_DELAY_MS.saturating_mul(2u64.saturating_pow(attempt)); @@ -22,9 +20,13 @@ pub struct JsonRpcRequest { } impl JsonRpcRequest { - pub fn new(id: u64, method: &'static str, params: T) -> Self { - Self { jsonrpc: "2.0", id, method, params } + Self { + jsonrpc: "2.0", + id, + method, + params, + } } } @@ -124,7 +126,12 @@ impl JsonRpcTransport { for attempt in 0..=self.max_retries { if attempt > 0 { let delay = backoff_duration(attempt); - tracing::debug!(attempt, method, delay_ms = delay.as_millis(), "backing off before retry"); + tracing::debug!( + attempt, + method, + delay_ms = delay.as_millis(), + "backing off before retry" + ); tokio::time::sleep(delay).await; tracing::debug!(attempt, method, "retrying RPC request"); } @@ -151,7 +158,9 @@ impl JsonRpcTransport { if status == reqwest::StatusCode::TOO_MANY_REQUESTS { crate::rpc::record_rpc_duration(method, duration_secs, false); tracing::warn!(method, attempt, "rate limited by RPC endpoint, will retry"); - last_error = Some(PrismError::RpcError(format!("rate limited (attempt {attempt})"))); + last_error = Some(PrismError::RpcError(format!( + "rate limited (attempt {attempt})" + ))); continue; } diff --git a/crates/core/src/rpc/metrics.rs b/crates/core/src/rpc/metrics.rs index 65767cb5..7381398c 100644 --- a/crates/core/src/rpc/metrics.rs +++ b/crates/core/src/rpc/metrics.rs @@ -1,5 +1,3 @@ - - use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::sync::{Mutex, OnceLock}; @@ -10,7 +8,6 @@ const BUCKETS: &[f64] = &[ #[derive(Debug, Serialize, Deserialize)] struct Histogram { - bucket_counts: Vec, count: u64, @@ -73,7 +70,6 @@ pub struct RpcMetricsRegistry { } impl RpcMetricsRegistry { - pub fn record(&mut self, method: &str, duration_secs: f64, success: bool) { let outcome = if success { "success" } else { "error" }; let key = format!("{method}:{outcome}"); @@ -96,9 +92,7 @@ impl RpcMetricsRegistry { for key in keys { let hist = &self.histograms[key]; - let (method, outcome) = key - .split_once(':') - .unwrap_or((key.as_str(), "unknown")); + let (method, outcome) = key.split_once(':').unwrap_or((key.as_str(), "unknown")); hist.render(method, outcome, &mut out); } @@ -181,16 +175,16 @@ mod tests { let mut h = Histogram::new(); h.observe(0.08); - let idx_005 = 0; - let idx_01 = 1; - let idx_025 = 2; - let idx_05 = 3; + let idx_005 = 0; + let idx_01 = 1; + let idx_025 = 2; + let idx_05 = 3; let idx_01_s = 4; // 0.1 - assert_eq!(h.bucket_counts[idx_005], 0); - assert_eq!(h.bucket_counts[idx_01], 0); - assert_eq!(h.bucket_counts[idx_025], 0); - assert_eq!(h.bucket_counts[idx_05], 0); + assert_eq!(h.bucket_counts[idx_005], 0); + assert_eq!(h.bucket_counts[idx_01], 0); + assert_eq!(h.bucket_counts[idx_025], 0); + assert_eq!(h.bucket_counts[idx_05], 0); assert_eq!(h.bucket_counts[idx_01_s], 1); } diff --git a/crates/core/src/spec/decoder.rs b/crates/core/src/spec/decoder.rs index 9809dccf..cdcd8772 100644 --- a/crates/core/src/spec/decoder.rs +++ b/crates/core/src/spec/decoder.rs @@ -1,12 +1,9 @@ - - use crate::error::{PrismError, PrismResult}; use serde::{Deserialize, Serialize}; -use stellar_xdr::curr::{ScSpecEntry, ScSpecTypeDef, Limits, ReadXdr, Limited}; +use stellar_xdr::curr::{Limited, Limits, ReadXdr, ScSpecEntry, ScSpecTypeDef}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ContractErrorEntry { - pub code: u32, pub name: String, @@ -16,7 +13,6 @@ pub struct ContractErrorEntry { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ContractFunction { - pub name: String, pub params: Vec<(String, String)>, @@ -28,7 +24,6 @@ pub struct ContractFunction { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ContractSpec { - pub errors: Vec, pub functions: Vec, @@ -81,7 +76,7 @@ pub fn decode_contract_spec(wasm_bytes: &[u8]) -> PrismResult { ScSpecEntry::UdtErrorEnumV0(err_enum) => { let enum_name = err_enum.name.to_string(); for case in err_enum.cases.iter() { - let case_name = format!("{}::{}", enum_name, case.name.to_string()); + let case_name = format!("{}::{}", enum_name, case.name); let doc = if case.doc.is_empty() { None } else { @@ -129,9 +124,17 @@ fn format_type_def(type_def: &ScSpecTypeDef) -> String { ScSpecTypeDef::Symbol => "Symbol".to_string(), ScSpecTypeDef::Address => "Address".to_string(), ScSpecTypeDef::Option(opt) => format!("Option<{}>", format_type_def(&opt.value_type)), - ScSpecTypeDef::Result(res) => format!("Result<{}, {}>", format_type_def(&res.ok_type), format_type_def(&res.error_type)), + ScSpecTypeDef::Result(res) => format!( + "Result<{}, {}>", + format_type_def(&res.ok_type), + format_type_def(&res.error_type) + ), ScSpecTypeDef::Vec(vec) => format!("Vec<{}>", format_type_def(&vec.element_type)), - ScSpecTypeDef::Map(map) => format!("Map<{}, {}>", format_type_def(&map.key_type), format_type_def(&map.value_type)), + ScSpecTypeDef::Map(map) => format!( + "Map<{}, {}>", + format_type_def(&map.key_type), + format_type_def(&map.value_type) + ), ScSpecTypeDef::Tuple(tuple) => { let elements: Vec = tuple.value_types.iter().map(format_type_def).collect(); format!("({})", elements.join(", ")) @@ -143,7 +146,6 @@ fn format_type_def(type_def: &ScSpecTypeDef) -> String { pub struct SpecParser; impl SpecParser { - pub fn extract_spec(wasm_bytes: &[u8]) -> PrismResult> { let parser = wasmparser::Parser::new(0); for payload in parser.parse_all(wasm_bytes) { @@ -198,7 +200,7 @@ mod tests { custom_payload.extend_from_slice(section_name.as_bytes()); custom_payload.extend_from_slice(§ion_data); - wasm.push(0); + wasm.push(0); wasm.push(custom_payload.len() as u8); wasm.extend(custom_payload); diff --git a/crates/core/src/spec/mod.rs b/crates/core/src/spec/mod.rs index d53d5a3c..56812db3 100644 --- a/crates/core/src/spec/mod.rs +++ b/crates/core/src/spec/mod.rs @@ -1,3 +1 @@ - - pub mod decoder; diff --git a/crates/core/src/taxonomy/mod.rs b/crates/core/src/taxonomy/mod.rs index 8327a950..3c500463 100644 --- a/crates/core/src/taxonomy/mod.rs +++ b/crates/core/src/taxonomy/mod.rs @@ -1,4 +1,2 @@ - - pub mod loader; pub mod schema; diff --git a/crates/core/src/types/address.rs b/crates/core/src/types/address.rs index 7a1e544c..c7b74a5c 100644 --- a/crates/core/src/types/address.rs +++ b/crates/core/src/types/address.rs @@ -1,5 +1,3 @@ - - use serde::{Deserialize, Serialize}; use std::fmt; use stellar_strkey::{ed25519::PublicKey, Contract, Strkey}; @@ -8,7 +6,6 @@ use crate::error::{PrismError, PrismResult}; #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct Address { - pub bytes: Vec, pub address_type: AddressType, @@ -16,14 +13,12 @@ pub struct Address { #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum AddressType { - Account, Contract, } impl Address { - pub fn new(bytes: Vec, address_type: AddressType) -> Self { Self { bytes, diff --git a/crates/core/src/types/config.rs b/crates/core/src/types/config.rs index cbb6222a..d4368412 100644 --- a/crates/core/src/types/config.rs +++ b/crates/core/src/types/config.rs @@ -1,5 +1,3 @@ - - pub use crate::network::config::{Network, NetworkConfig}; use serde::{Deserialize, Serialize}; @@ -7,7 +5,6 @@ pub const DEFAULT_REQUEST_TIMEOUT_SECS: u64 = 30; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PrismConfig { - pub default_network: Network, pub networks: Vec, @@ -51,6 +48,9 @@ mod tests { Network::Futurenet.default_rpc_url(), "https://rpc-futurenet.stellar.org" ); - assert_eq!(Network::Custom("local".to_string()).default_rpc_url(), "http://127.0.0.1:8000/rpc"); + assert_eq!( + Network::Custom("local".to_string()).default_rpc_url(), + "http://127.0.0.1:8000/rpc" + ); } } diff --git a/crates/core/src/types/mod.rs b/crates/core/src/types/mod.rs index 1be76a3f..d879adcb 100644 --- a/crates/core/src/types/mod.rs +++ b/crates/core/src/types/mod.rs @@ -1,5 +1,3 @@ - - pub mod address; pub mod config; pub mod report; diff --git a/crates/core/src/types/report.rs b/crates/core/src/types/report.rs index 48f709f0..dbfa3e35 100644 --- a/crates/core/src/types/report.rs +++ b/crates/core/src/types/report.rs @@ -1,5 +1,3 @@ - - use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] @@ -13,7 +11,6 @@ pub enum Severity { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RootCause { - pub description: String, pub likelihood: String, @@ -21,7 +18,6 @@ pub struct RootCause { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SuggestedFix { - pub description: String, pub difficulty: String, @@ -37,7 +33,6 @@ pub struct SuggestedFix { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ContractErrorInfo { - pub contract_id: String, pub error_code: u32, @@ -49,7 +44,6 @@ pub struct ContractErrorInfo { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TransactionContext { - pub tx_hash: String, pub ledger_sequence: u32, @@ -100,7 +94,6 @@ pub struct FailureAttribution { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DiagnosticReport { - pub error_category: String, pub error_code: u32, @@ -134,7 +127,6 @@ pub struct DiagnosticReport { } impl DiagnosticReport { - pub fn new(category: &str, code: u32, name: &str, summary: &str) -> Self { Self { error_category: category.to_string(), diff --git a/crates/core/src/types/trace.rs b/crates/core/src/types/trace.rs index b9440797..6c7134f5 100644 --- a/crates/core/src/types/trace.rs +++ b/crates/core/src/types/trace.rs @@ -1,11 +1,8 @@ - - use serde::{Deserialize, Serialize}; use std::collections::HashMap; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct HostFunctionCall { - pub function_name: String, pub arguments: Vec, @@ -23,7 +20,6 @@ pub struct HostFunctionCall { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ContractInvocation { - pub contract_id: String, pub function_name: String, @@ -45,7 +41,6 @@ pub struct ContractInvocation { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LedgerEntryDiff { - pub key: String, pub before: Option, @@ -65,13 +60,11 @@ pub enum DiffChangeType { #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct StateDiff { - pub entries: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ResourceHotspot { - pub location: String, pub cpu_instructions: u64, @@ -85,7 +78,6 @@ pub struct ResourceHotspot { #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct ResourceProfile { - pub total_cpu: u64, pub cpu_limit: u64, @@ -105,7 +97,6 @@ pub struct ResourceProfile { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DiagnosticEvent { - pub event_type: String, pub topics: Vec, @@ -117,7 +108,6 @@ pub struct DiagnosticEvent { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ExecutionTrace { - pub tx_hash: String, pub ledger_sequence: u32, diff --git a/crates/core/src/xdr/codec.rs b/crates/core/src/xdr/codec.rs index 48c4ff6d..49014a34 100644 --- a/crates/core/src/xdr/codec.rs +++ b/crates/core/src/xdr/codec.rs @@ -1,15 +1,12 @@ - - use crate::error::{PrismError, PrismResult}; use base64::{engine::general_purpose::STANDARD, Engine as _}; use stellar_xdr::curr::{ ContractEvent, DiagnosticEvent, LedgerEntry, LedgerKey, Limits, ReadXdr, ScAddress, ScBytes, - ScMap, ScMapEntry, ScString, ScSymbol, ScVal, ScVec, TransactionEnvelope, TransactionMeta, - TransactionResult, WriteXdr, SorobanAuthorizationEntry + ScMap, ScMapEntry, ScString, ScSymbol, ScVal, ScVec, SorobanAuthorizationEntry, + TransactionEnvelope, TransactionMeta, TransactionResult, WriteXdr, }; pub trait XdrCodec: Sized { - const TYPE_NAME: &'static str; /// Decode from XDR bytes. @@ -111,11 +108,9 @@ impl XdrCodec for LedgerEntry { const TYPE_NAME: &'static str = "LedgerEntry"; fn from_xdr_bytes(bytes: &[u8]) -> PrismResult { - LedgerEntry::from_xdr(bytes, Limits::none()).map_err(|e| { - PrismError::XdrDecodingFailed { - type_name: Self::TYPE_NAME, - reason: e.to_string(), - } + LedgerEntry::from_xdr(bytes, Limits::none()).map_err(|e| PrismError::XdrDecodingFailed { + type_name: Self::TYPE_NAME, + reason: e.to_string(), }) } @@ -149,11 +144,9 @@ impl XdrCodec for ScVec { const TYPE_NAME: &'static str = "ScVec"; fn from_xdr_bytes(bytes: &[u8]) -> PrismResult { - ScVec::from_xdr(bytes, Limits::none()).map_err(|e| { - PrismError::XdrDecodingFailed { - type_name: Self::TYPE_NAME, - reason: e.to_string(), - } + ScVec::from_xdr(bytes, Limits::none()).map_err(|e| PrismError::XdrDecodingFailed { + type_name: Self::TYPE_NAME, + reason: e.to_string(), }) } @@ -168,11 +161,9 @@ 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(), - } + ContractEvent::from_xdr(bytes, Limits::none()).map_err(|e| PrismError::XdrDecodingFailed { + type_name: Self::TYPE_NAME, + reason: e.to_string(), }) } @@ -187,11 +178,9 @@ 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(), - } + ScVal::from_xdr(bytes, Limits::none()).map_err(|e| PrismError::XdrDecodingFailed { + type_name: Self::TYPE_NAME, + reason: e.to_string(), }) } @@ -206,11 +195,9 @@ impl XdrCodec for ScAddress { const TYPE_NAME: &'static str = "ScAddress"; fn from_xdr_bytes(bytes: &[u8]) -> PrismResult { - ScAddress::from_xdr(bytes, Limits::none()).map_err(|e| { - PrismError::XdrDecodingFailed { - type_name: Self::TYPE_NAME, - reason: e.to_string(), - } + ScAddress::from_xdr(bytes, Limits::none()).map_err(|e| PrismError::XdrDecodingFailed { + type_name: Self::TYPE_NAME, + reason: e.to_string(), }) } @@ -225,11 +212,9 @@ impl XdrCodec for ScSymbol { const TYPE_NAME: &'static str = "ScSymbol"; fn from_xdr_bytes(bytes: &[u8]) -> PrismResult { - ScSymbol::from_xdr(bytes, Limits::none()).map_err(|e| { - PrismError::XdrDecodingFailed { - type_name: Self::TYPE_NAME, - reason: e.to_string(), - } + ScSymbol::from_xdr(bytes, Limits::none()).map_err(|e| PrismError::XdrDecodingFailed { + type_name: Self::TYPE_NAME, + reason: e.to_string(), }) } @@ -244,11 +229,9 @@ impl XdrCodec for ScString { const TYPE_NAME: &'static str = "ScString"; fn from_xdr_bytes(bytes: &[u8]) -> PrismResult { - ScString::from_xdr(bytes, Limits::none()).map_err(|e| { - PrismError::XdrDecodingFailed { - type_name: Self::TYPE_NAME, - reason: e.to_string(), - } + ScString::from_xdr(bytes, Limits::none()).map_err(|e| PrismError::XdrDecodingFailed { + type_name: Self::TYPE_NAME, + reason: e.to_string(), }) } @@ -263,11 +246,9 @@ impl XdrCodec for ScBytes { const TYPE_NAME: &'static str = "ScBytes"; fn from_xdr_bytes(bytes: &[u8]) -> PrismResult { - ScBytes::from_xdr(bytes, Limits::none()).map_err(|e| { - PrismError::XdrDecodingFailed { - type_name: Self::TYPE_NAME, - reason: e.to_string(), - } + ScBytes::from_xdr(bytes, Limits::none()).map_err(|e| PrismError::XdrDecodingFailed { + type_name: Self::TYPE_NAME, + reason: e.to_string(), }) } @@ -282,11 +263,9 @@ impl XdrCodec for ScMap { const TYPE_NAME: &'static str = "ScMap"; fn from_xdr_bytes(bytes: &[u8]) -> PrismResult { - ScMap::from_xdr(bytes, Limits::none()).map_err(|e| { - PrismError::XdrDecodingFailed { - type_name: Self::TYPE_NAME, - reason: e.to_string(), - } + ScMap::from_xdr(bytes, Limits::none()).map_err(|e| PrismError::XdrDecodingFailed { + type_name: Self::TYPE_NAME, + reason: e.to_string(), }) } @@ -301,11 +280,9 @@ impl XdrCodec for ScMapEntry { const TYPE_NAME: &'static str = "ScMapEntry"; fn from_xdr_bytes(bytes: &[u8]) -> PrismResult { - ScMapEntry::from_xdr(bytes, Limits::none()).map_err(|e| { - PrismError::XdrDecodingFailed { - type_name: Self::TYPE_NAME, - reason: e.to_string(), - } + ScMapEntry::from_xdr(bytes, Limits::none()).map_err(|e| PrismError::XdrDecodingFailed { + type_name: Self::TYPE_NAME, + reason: e.to_string(), }) } @@ -320,11 +297,9 @@ impl XdrCodec for LedgerKey { const TYPE_NAME: &'static str = "LedgerKey"; fn from_xdr_bytes(bytes: &[u8]) -> PrismResult { - LedgerKey::from_xdr(bytes, Limits::none()).map_err(|e| { - PrismError::XdrDecodingFailed { - type_name: Self::TYPE_NAME, - reason: e.to_string(), - } + LedgerKey::from_xdr(bytes, Limits::none()).map_err(|e| PrismError::XdrDecodingFailed { + type_name: Self::TYPE_NAME, + reason: e.to_string(), }) } @@ -337,9 +312,9 @@ impl XdrCodec for LedgerKey { /// 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| { - PrismError::XdrError(format!("Base64 decode failed: {e}")) - }) + STANDARD + .decode(xdr_base64) + .map_err(|e| PrismError::XdrError(format!("Base64 decode failed: {e}"))) } /// Encode raw bytes to a base64 XDR string. @@ -406,7 +381,8 @@ mod tests { fn test_xdr_codec_round_trip() { let envelope = make_test_envelope(); let b64 = crate::xdr::codec::XdrCodec::to_xdr_base64(&envelope).expect("encode"); - let decoded = ::from_xdr_base64(&b64).expect("decode"); + let decoded = ::from_xdr_base64(&b64) + .expect("decode"); assert_eq!(envelope, decoded); } @@ -427,18 +403,23 @@ mod tests { ext: ExtensionPoint::V0, contract_id: None, type_: stellar_xdr::curr::ContractEventType::Contract, - body: stellar_xdr::curr::ContractEventBody::V0(stellar_xdr::curr::ContractEventV0 { - topics: vec![].try_into().unwrap(), - data: stellar_xdr::curr::ScVal::Void, - }), - }].try_into().unwrap(), + body: stellar_xdr::curr::ContractEventBody::V0( + stellar_xdr::curr::ContractEventV0 { + topics: vec![].try_into().unwrap(), + data: stellar_xdr::curr::ScVal::Void, + }, + ), + }] + .try_into() + .unwrap(), return_value: stellar_xdr::curr::ScVal::Void, diagnostic_events: vec![].try_into().unwrap(), }), }); let b64 = crate::xdr::codec::XdrCodec::to_xdr_base64(&meta).expect("encode V3"); - let decoded = ::from_xdr_base64(&b64).expect("decode V3"); + let decoded = ::from_xdr_base64(&b64) + .expect("decode V3"); if let TransactionMeta::V3(v3) = decoded { assert_eq!(v3.operations.len(), 1); @@ -460,7 +441,8 @@ mod tests { let xdr_bytes = vec![0u8; 20]; let bytes = encode_xdr_base64(&xdr_bytes); - let decoded = ::from_xdr_base64(&bytes).expect("decode"); + let decoded = ::from_xdr_base64(&bytes) + .expect("decode"); let encoded = crate::xdr::codec::XdrCodec::to_xdr_base64(&decoded).expect("encode"); assert_eq!(bytes, encoded); @@ -474,28 +456,36 @@ mod tests { ext: ExtensionPoint::V0, contract_id: None, type_: stellar_xdr::curr::ContractEventType::Contract, - body: stellar_xdr::curr::ContractEventBody::V0(stellar_xdr::curr::ContractEventV0 { - topics: vec![].try_into().unwrap(), - data: stellar_xdr::curr::ScVal::Void, - }), + body: stellar_xdr::curr::ContractEventBody::V0( + stellar_xdr::curr::ContractEventV0 { + topics: vec![].try_into().unwrap(), + data: stellar_xdr::curr::ScVal::Void, + }, + ), }, }; let b64 = crate::xdr::codec::XdrCodec::to_xdr_base64(&event).expect("encode"); - let decoded = ::from_xdr_base64(&b64).expect("decode"); + let decoded = ::from_xdr_base64(&b64) + .expect("decode"); assert_eq!(event, decoded); } #[test] fn test_scvec_round_trip() { - let scvec = ScVec(vec![ - stellar_xdr::curr::ScVal::Void, - stellar_xdr::curr::ScVal::Bool(true), - stellar_xdr::curr::ScVal::U32(42), - ].try_into().unwrap()); + let scvec = ScVec( + vec![ + stellar_xdr::curr::ScVal::Void, + stellar_xdr::curr::ScVal::Bool(true), + stellar_xdr::curr::ScVal::U32(42), + ] + .try_into() + .unwrap(), + ); let b64 = crate::xdr::codec::XdrCodec::to_xdr_base64(&scvec).expect("encode"); - let decoded = ::from_xdr_base64(&b64).expect("decode"); + let decoded = + ::from_xdr_base64(&b64).expect("decode"); assert_eq!(scvec, decoded); } @@ -579,9 +569,7 @@ mod tests { scval_round_trip(ScVal::Symbol(ScSymbol( StringM::try_from(b"transfer".to_vec()).unwrap(), ))); - scval_round_trip(ScVal::Symbol(ScSymbol( - StringM::try_from(vec![]).unwrap(), - ))); + scval_round_trip(ScVal::Symbol(ScSymbol(StringM::try_from(vec![]).unwrap()))); } #[test] @@ -641,12 +629,8 @@ mod tests { fn test_scval_ledger_key_nonce_round_trip() { use stellar_xdr::curr::ScNonceKey; scval_round_trip(ScVal::LedgerKeyNonce(ScNonceKey { nonce: 0 })); - scval_round_trip(ScVal::LedgerKeyNonce(ScNonceKey { - nonce: i64::MIN, - })); - scval_round_trip(ScVal::LedgerKeyNonce(ScNonceKey { - nonce: i64::MAX, - })); + scval_round_trip(ScVal::LedgerKeyNonce(ScNonceKey { nonce: i64::MIN })); + scval_round_trip(ScVal::LedgerKeyNonce(ScNonceKey { nonce: i64::MAX })); } #[test] @@ -713,7 +697,8 @@ mod tests { fn test_scmap_empty_round_trip() { let map = ScMap(vec![].try_into().unwrap()); let b64 = crate::xdr::codec::XdrCodec::to_xdr_base64(&map).expect("encode"); - let decoded = ::from_xdr_base64(&b64).expect("decode"); + let decoded = + ::from_xdr_base64(&b64).expect("decode"); assert_eq!(map, decoded); assert_eq!(decoded.0.len(), 0); } @@ -726,16 +711,22 @@ mod tests { }; // ScMapEntry round-trip let b64 = crate::xdr::codec::XdrCodec::to_xdr_base64(&entry).expect("encode"); - let decoded_entry = ::from_xdr_base64(&b64).expect("decode"); + let decoded_entry = + ::from_xdr_base64(&b64).expect("decode"); assert_eq!(entry, decoded_entry); // ScMap with single entry - let map = ScMap::sorted_from_entries(vec![ScMapEntry { - key: ScVal::U32(1), - val: ScVal::Bool(true), - }].into_iter()).expect("sorted_from_entries"); + let map = ScMap::sorted_from_entries( + vec![ScMapEntry { + key: ScVal::U32(1), + val: ScVal::Bool(true), + }] + .into_iter(), + ) + .expect("sorted_from_entries"); let b64 = crate::xdr::codec::XdrCodec::to_xdr_base64(&map).expect("encode"); - let decoded = ::from_xdr_base64(&b64).expect("decode"); + let decoded = + ::from_xdr_base64(&b64).expect("decode"); assert_eq!(map, decoded); assert_eq!(decoded.0.len(), 1); assert_eq!(decoded.0[0].key, ScVal::U32(1)); @@ -746,13 +737,23 @@ mod tests { fn test_scmap_multi_entry_round_trip() { // Entries inserted out of order — sorted_from_entries must sort them by key. let entries = vec![ - ScMapEntry { key: ScVal::U32(3), val: ScVal::Bool(false) }, - ScMapEntry { key: ScVal::U32(1), val: ScVal::Void }, - ScMapEntry { key: ScVal::U32(2), val: ScVal::Bool(true) }, + ScMapEntry { + key: ScVal::U32(3), + val: ScVal::Bool(false), + }, + ScMapEntry { + key: ScVal::U32(1), + val: ScVal::Void, + }, + ScMapEntry { + key: ScVal::U32(2), + val: ScVal::Bool(true), + }, ]; let map = ScMap::sorted_from_entries(entries.into_iter()).expect("sorted_from_entries"); let b64 = crate::xdr::codec::XdrCodec::to_xdr_base64(&map).expect("encode"); - let decoded = ::from_xdr_base64(&b64).expect("decode"); + let decoded = + ::from_xdr_base64(&b64).expect("decode"); assert_eq!(map, decoded); assert_eq!(decoded.0.len(), 3); // Keys must be in ascending order after sorting. diff --git a/crates/core/src/xdr/diff.rs b/crates/core/src/xdr/diff.rs new file mode 100644 index 00000000..289a2081 --- /dev/null +++ b/crates/core/src/xdr/diff.rs @@ -0,0 +1,164 @@ +use std::fmt; +use stellar_xdr::next::{AccountEntry, Signer}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct AccountEntryDiff { + pub balance_change: i64, + pub seq_num_change: i64, + pub num_sub_entries_change: i32, + pub added_signers: Vec, + pub removed_signers: Vec, +} + +impl fmt::Display for AccountEntryDiff { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + writeln!(f, "AccountEntry Diff:")?; + + if self.balance_change != 0 { + let direction = if self.balance_change > 0 { "+" } else { "" }; + writeln!( + f, + " 💰 Balance: {}{} XLM (stroops)", + direction, self.balance_change + )?; + } + + if self.seq_num_change != 0 { + let direction = if self.seq_num_change > 0 { "+" } else { "" }; + writeln!(f, " 🔢 Sequence: {}{}", direction, self.seq_num_change)?; + } + + if self.num_sub_entries_change != 0 { + let direction = if self.num_sub_entries_change > 0 { + "+" + } else { + "" + }; + writeln!( + f, + " 📦 Sub-entries: {}{}", + direction, self.num_sub_entries_change + )?; + } + + if !self.added_signers.is_empty() { + writeln!(f, " 🔐 Added Signers: {}", self.added_signers.len())?; + for signer in &self.added_signers { + writeln!(f, " - {signer:?}")?; + } + } + + if !self.removed_signers.is_empty() { + writeln!(f, " 🔓 Removed Signers: {}", self.removed_signers.len())?; + for signer in &self.removed_signers { + writeln!(f, " - {signer:?}")?; + } + } + + Ok(()) + } +} + +pub fn diff_account_entry(old: &AccountEntry, new: &AccountEntry) -> AccountEntryDiff { + let balance_change = new.balance - old.balance; + let seq_num_change = new.seq_num.0 - old.seq_num.0; + + let old_sub = old.num_sub_entries.cast_signed(); + let new_sub = new.num_sub_entries.cast_signed(); + let num_sub_entries_change = new_sub - old_sub; + + let mut added_signers = Vec::new(); + let mut removed_signers = Vec::new(); + + let old_signers = old.signers.as_vec(); + let new_signers = new.signers.as_vec(); + + for signer in new_signers { + if !old_signers.contains(signer) { + added_signers.push(signer.clone()); + } + } + + for signer in old_signers { + if !new_signers.contains(signer) { + removed_signers.push(signer.clone()); + } + } + + AccountEntryDiff { + balance_change, + seq_num_change, + num_sub_entries_change, + added_signers, + removed_signers, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use stellar_xdr::next::{ + AccountEntryExt, AccountId, PublicKey, SequenceNumber, SignerKey, String32, StringM, Thresholds, + Uint256, VecM, + }; + + fn dummy_account_entry() -> AccountEntry { + AccountEntry { + account_id: AccountId(PublicKey::PublicKeyTypeEd25519(Uint256([0; 32]))), + balance: 1000, + seq_num: SequenceNumber(10), + num_sub_entries: 2, + inflation_dest: None, + flags: 0, + home_domain: String32(StringM::default()), + thresholds: Thresholds([0; 4]), + signers: VecM::default(), + ext: AccountEntryExt::V0, + } + } + + #[test] + fn test_diff_balance_and_seq() { + let old = dummy_account_entry(); + let mut new = old.clone(); + new.balance = 800; // 200 fee deduction + new.seq_num = SequenceNumber(11); // +1 seq + + let diff = diff_account_entry(&old, &new); + assert_eq!(diff.balance_change, -200); + assert_eq!(diff.seq_num_change, 1); + assert_eq!(diff.num_sub_entries_change, 0); + assert!(diff.added_signers.is_empty()); + assert!(diff.removed_signers.is_empty()); + } + + #[test] + fn test_diff_signers() { + let mut old = dummy_account_entry(); + let mut new = dummy_account_entry(); + + let signer1 = Signer { + key: SignerKey::Ed25519(Uint256([1; 32])), + weight: 1, + }; + let signer2 = Signer { + key: SignerKey::Ed25519(Uint256([2; 32])), + weight: 2, + }; + + let mut old_signers = old.signers.to_vec(); + old_signers.push(signer1.clone()); + old.signers = old_signers.try_into().unwrap(); + + let mut new_signers = new.signers.to_vec(); + new_signers.push(signer2.clone()); + new.signers = new_signers.try_into().unwrap(); + + let diff = diff_account_entry(&old, &new); + assert_eq!(diff.added_signers.len(), 1); + assert_eq!(diff.added_signers[0], signer2); + + assert_eq!(diff.removed_signers.len(), 1); + assert_eq!(diff.removed_signers[0], signer1); + } +} diff --git a/crates/core/src/xdr/mod.rs b/crates/core/src/xdr/mod.rs index cd9a18d9..306f2243 100644 --- a/crates/core/src/xdr/mod.rs +++ b/crates/core/src/xdr/mod.rs @@ -1,3 +1,2 @@ - - pub mod codec; +pub mod diff; diff --git a/crates/core/src/xdr/scratch.rs b/crates/core/src/xdr/scratch.rs new file mode 100644 index 00000000..bf3b2e72 --- /dev/null +++ b/crates/core/src/xdr/scratch.rs @@ -0,0 +1,8 @@ +use stellar_xdr::next::{AccountEntry, SequenceNumber}; + +pub fn check_fields(entry: &AccountEntry) { + let _bal = entry.balance; + let _seq = entry.seq_num; + let _num = entry.num_sub_entries; + let _sig = &entry.signers; +}