From 02284b2f7ce8899f8fdec8c5a13dff4665abe8ec Mon Sep 17 00:00:00 2001 From: shogun444 Date: Wed, 17 Jun 2026 20:30:10 +0530 Subject: [PATCH] feat: split transaction fee into inclusion and resource components --- crates/cli/src/commands/inspect.rs | 2 +- crates/cli/src/output/renderers.rs | 1 + crates/core/src/decode/context.rs | 191 ++++++++++++++++++++++++++--- crates/core/src/types/report.rs | 1 + scratch/xdr_test/src/main.rs | 17 ++- 5 files changed, 194 insertions(+), 18 deletions(-) diff --git a/crates/cli/src/commands/inspect.rs b/crates/cli/src/commands/inspect.rs index 1f85c6b1..18597145 100644 --- a/crates/cli/src/commands/inspect.rs +++ b/crates/cli/src/commands/inspect.rs @@ -45,7 +45,7 @@ pub async fn run( { let fee_context = report.transaction_context.as_ref().map(|ctx| &ctx.fee); - let bid_fee: Option = None; + let bid_fee = fee_context.and_then(|fee| fee.bid_fee); let resource_fee = fee_context.map(|fee| fee.resource_fee); let total_charged_fee = fee_context.and_then(|fee| fee.inclusion_fee.checked_add(fee.resource_fee)); diff --git a/crates/cli/src/output/renderers.rs b/crates/cli/src/output/renderers.rs index 15f5830e..568ead71 100644 --- a/crates/cli/src/output/renderers.rs +++ b/crates/cli/src/output/renderers.rs @@ -482,6 +482,7 @@ mod tests { resource_fee: 50, refundable_fee: 25, non_refundable_fee: 25, + bid_fee: Some(150), }, resources: ResourceSummary { cpu_instructions_used: 1000, diff --git a/crates/core/src/decode/context.rs b/crates/core/src/decode/context.rs index 6f7eea48..25208ad2 100644 --- a/crates/core/src/decode/context.rs +++ b/crates/core/src/decode/context.rs @@ -44,23 +44,76 @@ fn extract_arguments(tx_data: &serde_json::Value) -> Vec { } fn extract_fee_breakdown(tx_data: &serde_json::Value) -> FeeBreakdown { + use crate::xdr::codec::XdrCodec; + use stellar_xdr::curr::{TransactionEnvelope, TransactionResult, TransactionMeta}; + + // 1. Get total fee from resultXdr + let mut total_fee = 0; + if let Some(result_xdr_b64) = tx_data.get("resultXdr").and_then(|v| v.as_str()) { + if let Ok(tx_result) = TransactionResult::from_xdr_base64(result_xdr_b64) { + total_fee = tx_result.fee_charged; + } + } + + // 2. Get bid fee from envelopeXdr + let mut bid_fee = None; + if let Some(envelope_xdr_b64) = tx_data.get("envelopeXdr").and_then(|v| v.as_str()) { + 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); + } + TransactionEnvelope::TxFeeBump(fee_bump) => { + bid_fee = Some(fee_bump.tx.fee as i64); + } + TransactionEnvelope::TxV0(v0) => { + bid_fee = Some(v0.tx.fee as i64); + } + } + } + } + + // 3. Get resource fee components from resultMetaXdr + let mut non_refundable_fee = 0; + let mut refundable_fee = 0; + let mut rent_fee = 0; + let mut has_soroban_meta = false; + + 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; + } + } + } + } + _ => {} + } + } + } + + let resource_fee = if has_soroban_meta { + non_refundable_fee + refundable_fee + rent_fee + } else { + 0 + }; + + let inclusion_fee = total_fee - resource_fee; + FeeBreakdown { - inclusion_fee: tx_data - .get("inclusionFee") - .and_then(serde_json::Value::as_i64) - .unwrap_or(0), - resource_fee: tx_data - .get("resourceFee") - .and_then(serde_json::Value::as_i64) - .unwrap_or(0), - refundable_fee: tx_data - .get("refundableFee") - .and_then(serde_json::Value::as_i64) - .unwrap_or(0), - non_refundable_fee: tx_data - .get("nonRefundableFee") - .and_then(serde_json::Value::as_i64) - .unwrap_or(0), + inclusion_fee, + resource_fee, + refundable_fee: refundable_fee + rent_fee, + non_refundable_fee, + bid_fee, } } @@ -74,3 +127,109 @@ fn extract_resource_summary(_tx_data: &serde_json::Value) -> ResourceSummary { write_bytes: 0, } } + +#[cfg(test)] +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, + }; + + #[test] + fn test_extract_fee_breakdown_non_soroban() { + let tx = Transaction { + source_account: MuxedAccount::Ed25519(Uint256([0; 32])), + fee: 150, + seq_num: SequenceNumber(1), + cond: Preconditions::None, + memo: Memo::None, + operations: vec![].try_into().unwrap(), + ext: TransactionExt::V0, + }; + let envelope = TransactionEnvelope::Tx(TransactionV1Envelope { + tx, + signatures: vec![].try_into().unwrap(), + }); + let envelope_xdr = envelope.to_xdr_base64().unwrap(); + + let result = TransactionResult { + fee_charged: 120, + result: TransactionResultResult::TxSuccess(vec![].try_into().unwrap()), + ext: stellar_xdr::curr::TransactionResultExt::V0, + }; + let result_xdr = result.to_xdr_base64().unwrap(); + + let tx_data = serde_json::json!({ + "envelopeXdr": envelope_xdr, + "resultXdr": result_xdr, + }); + + let breakdown = extract_fee_breakdown(&tx_data); + assert_eq!(breakdown.bid_fee, Some(150)); + assert_eq!(breakdown.inclusion_fee, 120); + assert_eq!(breakdown.resource_fee, 0); + assert_eq!(breakdown.refundable_fee, 0); + assert_eq!(breakdown.non_refundable_fee, 0); + } + + #[test] + fn test_extract_fee_breakdown_soroban() { + let tx = Transaction { + source_account: MuxedAccount::Ed25519(Uint256([0; 32])), + fee: 500, + seq_num: SequenceNumber(1), + cond: Preconditions::None, + memo: Memo::None, + operations: vec![].try_into().unwrap(), + ext: TransactionExt::V0, + }; + let envelope = TransactionEnvelope::Tx(TransactionV1Envelope { + tx, + signatures: vec![].try_into().unwrap(), + }); + let envelope_xdr = envelope.to_xdr_base64().unwrap(); + + let result = TransactionResult { + fee_charged: 450, + result: TransactionResultResult::TxSuccess(vec![].try_into().unwrap()), + ext: stellar_xdr::curr::TransactionResultExt::V0, + }; + let result_xdr = result.to_xdr_base64().unwrap(); + + let meta = TransactionMeta::V3(TransactionMetaV3 { + ext: ExtensionPoint::V0, + tx_changes_before: vec![].try_into().unwrap(), + operations: vec![].try_into().unwrap(), + tx_changes_after: vec![].try_into().unwrap(), + soroban_meta: Some(SorobanTransactionMeta { + ext: SorobanTransactionMetaExt::V1(SorobanTransactionMetaExtV1 { + ext: ExtensionPoint::V0, + total_non_refundable_resource_fee_charged: 100, + total_refundable_resource_fee_charged: 200, + rent_fee_charged: 50, + }), + events: vec![].try_into().unwrap(), + return_value: stellar_xdr::curr::ScVal::Void, + diagnostic_events: vec![].try_into().unwrap(), + }), + }); + let meta_xdr = meta.to_xdr_base64().unwrap(); + + let tx_data = serde_json::json!({ + "envelopeXdr": envelope_xdr, + "resultXdr": result_xdr, + "resultMetaXdr": meta_xdr, + }); + + let breakdown = extract_fee_breakdown(&tx_data); + assert_eq!(breakdown.bid_fee, Some(500)); + assert_eq!(breakdown.resource_fee, 350); // 100 + 200 + 50 + assert_eq!(breakdown.inclusion_fee, 100); // 450 - 350 + assert_eq!(breakdown.refundable_fee, 250); // 200 + 50 + assert_eq!(breakdown.non_refundable_fee, 100); + } +} diff --git a/crates/core/src/types/report.rs b/crates/core/src/types/report.rs index 232f3d47..209f73c5 100644 --- a/crates/core/src/types/report.rs +++ b/crates/core/src/types/report.rs @@ -69,6 +69,7 @@ pub struct FeeBreakdown { pub resource_fee: i64, pub refundable_fee: i64, pub non_refundable_fee: i64, + pub bid_fee: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/scratch/xdr_test/src/main.rs b/scratch/xdr_test/src/main.rs index e7a11a96..d85741cf 100644 --- a/scratch/xdr_test/src/main.rs +++ b/scratch/xdr_test/src/main.rs @@ -1,3 +1,18 @@ +use stellar_xdr::curr::TransactionEnvelope; + fn main() { - println!("Hello, world!"); + // We match on TransactionEnvelope to verify variants and fields. + // This is to make sure we can extract the max bid fee correctly. + let env = TransactionEnvelope::Tx(todo!()); + match env { + TransactionEnvelope::Tx(ref envelope) => { + let _ = envelope.tx.fee; + } + TransactionEnvelope::TxFeeBump(ref envelope) => { + let _ = envelope.tx.fee; + } + TransactionEnvelope::TxV0(ref envelope) => { + let _ = envelope.tx.fee; + } + } }