Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
224 changes: 222 additions & 2 deletions apps/web/src/components/decode/ContextSummary.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,223 @@
export default function ContextSummary() {
return <div>{/* TX context: function, args, auth, resources */}</div>;
import type { TransactionContext, FeeBreakdown } from "@/lib/types";

interface Props {
context: TransactionContext;
}

/** Formats a stroop value as a human-readable string. */
function formatFee(stroops: number): string {
return `${stroops.toLocaleString()} stroops`;
}

function formatOptFee(stroops?: number): string {
return stroops !== undefined ? formatFee(stroops) : "N/A";
}

function FeeRow({
label,
value,
sub,
variant = "default",
}: {
label: string;
value: string;
sub?: boolean;
variant?: "default" | "accent" | "success" | "warning" | "muted" | "meta";
}) {
const variantClass: Record<string, string> = {
default: "text-gray-200",
accent: "text-white font-semibold",
success: "text-green-400",
warning: "text-yellow-400",
muted: "text-gray-400",
meta: "text-blue-300",
};

return (
<div className={`flex justify-between ${sub ? "pl-6" : ""}`}>
<span className="text-gray-400">{label}</span>
<span className={variantClass[variant]}>{value}</span>
</div>
);
}

function FeeBreakdownPanel({ fee }: { fee: FeeBreakdown }) {
return (
<section aria-labelledby="fee-breakdown-heading">
<h3
id="fee-breakdown-heading"
className="text-xs font-semibold uppercase tracking-wider text-gray-500 mb-2"
>
Fee Breakdown
</h3>
<div className="space-y-1 text-sm">
<FeeRow
label="Bid Fee"
value={formatOptFee(fee.bid_fee)}
variant="meta"
/>
<FeeRow
label="Total Charged Fee"
value={formatFee(fee.total_charged_fee)}
variant="accent"
/>
<FeeRow
label="Inclusion Fee"
value={formatFee(fee.inclusion_fee)}
variant="success"
/>
<FeeRow
label="Resource Fee"
value={formatFee(fee.resource_fee)}
variant="warning"
/>
{fee.resource_fee > 0 && (
<>
<FeeRow
label="Refundable Resource Fee"
value={formatFee(fee.refundable_resource_fee)}
sub
variant="muted"
/>
<FeeRow
label="Non-Refundable Resource Fee"
value={formatFee(fee.non_refundable_fee)}
sub
variant="muted"
/>
</>
)}
</div>
{fee.resource_fee > 0 && (
<p className="mt-2 text-xs text-gray-500">
The refundable portion ({formatFee(fee.refundable_resource_fee)}) may be
partially returned if unused resources are reclaimed after execution.
</p>
)}
</section>
);
}

function ResourcePanel({ context }: { context: TransactionContext }) {
const { resources } = context;
const cpuPct =
resources.cpu_instructions_limit > 0
? Math.min(
(resources.cpu_instructions_used / resources.cpu_instructions_limit) * 100,
100
)
: 0;
const memPct =
resources.memory_bytes_limit > 0
? Math.min(
(resources.memory_bytes_used / resources.memory_bytes_limit) * 100,
100
)
: 0;

function barColor(pct: number): string {
if (pct >= 90) return "bg-red-500";
if (pct >= 70) return "bg-yellow-400";
return "bg-green-500";
}

function UsageBar({
label,
used,
limit,
pct,
}: {
label: string;
used: number;
limit: number;
pct: number;
}) {
return (
<div>
<div className="flex justify-between text-xs text-gray-400 mb-1">
<span>{label}</span>
<span>
{used.toLocaleString()} / {limit.toLocaleString()} ({pct.toFixed(0)}%)
</span>
</div>
<div
className="w-full h-2 rounded bg-gray-700"
role="progressbar"
aria-valuenow={pct}
aria-valuemin={0}
aria-valuemax={100}
aria-label={`${label} usage`}
>
<div
className={`h-2 rounded ${barColor(pct)}`}
style={{ width: `${pct}%` }}
/>
</div>
</div>
);
}

return (
<section aria-labelledby="resource-usage-heading">
<h3
id="resource-usage-heading"
className="text-xs font-semibold uppercase tracking-wider text-gray-500 mb-2"
>
Resource Usage
</h3>
<div className="space-y-3">
<UsageBar
label="CPU Instructions"
used={resources.cpu_instructions_used}
limit={resources.cpu_instructions_limit}
pct={cpuPct}
/>
<UsageBar
label="Memory"
used={resources.memory_bytes_used}
limit={resources.memory_bytes_limit}
pct={memPct}
/>
</div>
</section>
);
}

export default function ContextSummary({ context }: Props) {
return (
<div className="rounded-lg border border-gray-700 bg-gray-900 p-4 space-y-6 text-sm">
{/* Transaction metadata */}
<section aria-labelledby="tx-summary-heading">
<h3
id="tx-summary-heading"
className="text-xs font-semibold uppercase tracking-wider text-gray-500 mb-2"
>
Transaction Summary
</h3>
<div className="space-y-1">
<div className="flex justify-between">
<span className="text-gray-400">TX Hash</span>
<span className="font-mono text-xs text-gray-200 truncate max-w-[60%]">
{context.tx_hash}
</span>
</div>
<div className="flex justify-between">
<span className="text-gray-400">Ledger</span>
<span className="text-gray-200">{context.ledger_sequence}</span>
</div>
{context.function_name && (
<div className="flex justify-between">
<span className="text-gray-400">Function</span>
<span className="font-mono text-xs text-gray-200">
{context.function_name}
</span>
</div>
)}
</div>
</section>

<FeeBreakdownPanel fee={context.fee} />
<ResourcePanel context={context} />
</div>
);
}
28 changes: 28 additions & 0 deletions apps/web/src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,39 @@ export interface ContractErrorInfo {
doc_comment?: string;
}

export interface FeeBreakdown {
total_charged_fee: number;
inclusion_fee: number;
resource_fee: number;
/**
* The refundable portion of the resource fee
* (`total_refundable_resource_fee_charged` from SorobanTransactionMetaExtV1).
* This may be partially returned if unused resources are reclaimed after execution.
*/
refundable_resource_fee: number;
/** refundable_resource_fee + rent_fee_charged. Kept for backwards compatibility. */
refundable_fee: number;
non_refundable_fee: number;
bid_fee?: number;
}

export interface ResourceSummary {
cpu_instructions_used: number;
cpu_instructions_limit: number;
memory_bytes_used: number;
memory_bytes_limit: number;
read_bytes: number;
write_bytes: number;
}

export interface TransactionContext {
tx_hash: string;
ledger_sequence: number;
function_name?: string;
arguments: string[];
return_value?: string;
fee: FeeBreakdown;
resources: ResourceSummary;
}

export type Network = "mainnet" | "testnet" | "futurenet" | "custom";
9 changes: 6 additions & 3 deletions crates/cli/src/output/renderers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -455,11 +455,11 @@ pub fn render_fee_breakdown(fee: &FeeBreakdown) -> String {

if fee.resource_fee > 0 {
out.push_str(&format!(
" Refundable: {}\n",
palette.muted_text(&format_fee(fee.refundable_fee))
" Refundable Resource Fee: {}\n",
palette.muted_text(&format_fee(fee.refundable_resource_fee))
));
out.push_str(&format!(
" Non-Refundable: {}\n",
" Non-Refundable: {}\n",
palette.muted_text(&format_fee(fee.non_refundable_fee))
));
}
Expand Down Expand Up @@ -574,6 +574,7 @@ mod tests {
total_charged_fee: 150,
inclusion_fee: 100,
resource_fee: 50,
refundable_resource_fee: 25,
refundable_fee: 25,
non_refundable_fee: 25,
bid_fee: Some(150),
Expand All @@ -600,6 +601,7 @@ mod tests {
total_charged_fee: 150,
inclusion_fee: 100,
resource_fee: 50,
refundable_resource_fee: 25,
refundable_fee: 25,
non_refundable_fee: 25,
bid_fee: Some(150),
Expand All @@ -608,5 +610,6 @@ mod tests {
assert!(output.contains("FEE BREAKDOWN"));
assert!(output.contains("Total Charged Fee:"));
assert!(output.contains("150 stroops"));
assert!(output.contains("Refundable Resource Fee:"));
}
}
13 changes: 8 additions & 5 deletions crates/core/src/decode/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,15 +95,15 @@ fn extract_fee_breakdown(tx_data: &serde_json::Value) -> FeeBreakdown {
if let Some(resource_fee_obj) = tx_data.get("resourceFee").and_then(|v| v.as_object()) {
non_refundable_fee = resource_fee_obj
.get("totalNonRefundableResourceFeeCharged")
.and_then(|v| v.as_i64)
.and_then(|v| v.as_i64())
.unwrap_or(0);
refundable_fee = resource_fee_obj
.get("totalRefundableResourceFeeCharged")
.and_then(|v| v.as_i64)
.and_then(|v| v.as_i64())
.unwrap_or(0);
rent_fee = resource_fee_obj
.get("rentFeeCharged")
.and_then(|v| v.as_i64)
.and_then(|v| v.as_i64())
.unwrap_or(0);
has_soroban_meta = true;
} else if let Some(meta_xdr_b64) = tx_data.get("resultMetaXdr").and_then(|v| v.as_str()) {
Expand Down Expand Up @@ -135,13 +135,14 @@ fn extract_fee_breakdown(tx_data: &serde_json::Value) -> FeeBreakdown {

let inclusion_fee = tx_data
.get("inclusionFee")
.and_then(|v| v.as_i64)
.and_then(|v| v.as_i64())
.unwrap_or(total_fee - resource_fee);

FeeBreakdown {
total_charged_fee: total_fee,
inclusion_fee,
resource_fee,
refundable_resource_fee: refundable_fee,
refundable_fee: refundable_fee + rent_fee,
non_refundable_fee,
bid_fee,
Expand Down Expand Up @@ -248,6 +249,7 @@ mod tests {
assert_eq!(breakdown.bid_fee, Some(150));
assert_eq!(breakdown.inclusion_fee, 120);
assert_eq!(breakdown.resource_fee, 0);
assert_eq!(breakdown.refundable_resource_fee, 0);
assert_eq!(breakdown.refundable_fee, 0);
assert_eq!(breakdown.non_refundable_fee, 0);
}
Expand Down Expand Up @@ -306,7 +308,8 @@ mod tests {
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.refundable_resource_fee, 200); // total_refundable_resource_fee_charged only
assert_eq!(breakdown.refundable_fee, 250); // 200 + 50 (includes rent)
assert_eq!(breakdown.non_refundable_fee, 100);
}
}
7 changes: 7 additions & 0 deletions crates/core/src/types/report.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,14 @@ pub struct TransactionContext {
pub struct FeeBreakdown {
pub total_charged_fee: i64,
pub inclusion_fee: i64,
/// Total resource fee: non_refundable + refundable_resource_fee + rent_fee_charged.
pub resource_fee: i64,
/// The refundable portion of the resource fee charged
/// (`total_refundable_resource_fee_charged` from `SorobanTransactionMetaExtV1`).
/// This amount may be partially returned to the submitter if resources are unused.
pub refundable_resource_fee: i64,
/// `total_refundable_resource_fee_charged + rent_fee_charged`.
/// Kept for backwards compatibility with existing callers.
pub refundable_fee: i64,
pub non_refundable_fee: i64,
pub bid_fee: Option<i64>,
Expand Down