diff --git a/src/app/(sidebar)/transaction-dashboard/components/CallStackTrace.tsx b/src/app/(sidebar)/transaction-dashboard/components/CallStackTrace.tsx
new file mode 100644
index 000000000..9bd5d5fc7
--- /dev/null
+++ b/src/app/(sidebar)/transaction-dashboard/components/CallStackTrace.tsx
@@ -0,0 +1,641 @@
+import React, { JSX, useState } from "react";
+import { Alert, Icon, Label, Text, Toggle } from "@stellar/design-system";
+
+import { Box } from "@/components/layout/Box";
+import { SdsLink } from "@/components/SdsLink";
+import { TransactionTabEmptyMessage } from "@/components/TransactionTabEmptyMessage";
+
+import {
+ DiagnosticEventJson,
+ formatDiagnosticEvents,
+ FormattedEventData,
+ ProcessedEvent,
+} from "@/helpers/formatDiagnosticEvents";
+import { shortenStellarAddress } from "@/helpers/shortenStellarAddress";
+import { getStellarExpertNetwork } from "@/helpers/getStellarExpertNetwork";
+import { buildContractExplorerHref } from "@/helpers/buildContractExplorerHref";
+
+import { getContractIdError } from "@/validate/methods/getContractIdError";
+import { useStore } from "@/store/useStore";
+import { STELLAR_EXPERT } from "@/constants/settings";
+
+import { AnyObject, NetworkType } from "@/types/types";
+
+export const CallStackTrace = ({
+ diagnosticEvents,
+}: {
+ diagnosticEvents: DiagnosticEventJson[] | AnyObject | undefined;
+}) => {
+ const { network } = useStore();
+
+ const data =
+ diagnosticEvents && Array.isArray(diagnosticEvents)
+ ? formatDiagnosticEvents(diagnosticEvents)
+ : null;
+
+ const [isCollapsedView, setIsCollapsedView] = useState(false);
+
+ if (!data?.callStack?.length) {
+ return (
+
+ This transaction has no call stack trace.
+
+ );
+ }
+
+ const truncateParams = (
+ data: FormattedEventData[],
+ maxItems: number,
+ ): FormattedEventData[] => {
+ let itemCount = 0;
+ let ellipsisAdded = false;
+
+ const isContainer = (type: string): boolean => {
+ return type === "vec" || type === "map";
+ };
+
+ const addEllipsisIfNeeded = (items: any[], wasTruncated: boolean) => {
+ if (wasTruncated && !ellipsisAdded && items.length > 0) {
+ items.push({ value: "...", type: "ellipsis" });
+ ellipsisAdded = true;
+ }
+ };
+
+ const truncateArray = (items: any[]) => {
+ const truncatedArray: any[] = [];
+ let wasTruncated = false;
+
+ for (const item of items) {
+ if (itemCount >= maxItems) {
+ wasTruncated = true;
+ break;
+ }
+
+ const result = traverse(item);
+
+ if (result !== undefined) {
+ truncatedArray.push(result);
+ }
+ }
+
+ addEllipsisIfNeeded(truncatedArray, wasTruncated);
+
+ return {
+ truncatedArray,
+ wasTruncated,
+ };
+ };
+
+ const traverse = (node: any): any => {
+ if (
+ node &&
+ typeof node === "object" &&
+ "type" in node &&
+ "value" in node
+ ) {
+ const isContainerType = isContainer(node.type);
+
+ if (!isContainerType) {
+ itemCount++;
+
+ if (itemCount > maxItems) {
+ return undefined;
+ }
+
+ return node;
+ }
+
+ if (Array.isArray(node.value)) {
+ const { truncatedArray } = truncateArray(node.value);
+
+ if (truncatedArray.length > 0) {
+ return { ...node, value: truncatedArray };
+ }
+
+ return undefined;
+ }
+
+ return node;
+ }
+
+ if (Array.isArray(node)) {
+ const { truncatedArray } = truncateArray(node);
+
+ return truncatedArray.length > 0 ? truncatedArray : undefined;
+ }
+
+ return node;
+ };
+
+ const result: FormattedEventData[] | undefined = traverse(data);
+
+ return result || [];
+ };
+
+ const hasEllipsisAnywhere = (data: any): boolean => {
+ if (!data) return false;
+
+ if (Array.isArray(data)) {
+ return data.some((item) => {
+ if (item?.type === "ellipsis") return true;
+
+ if (Array.isArray(item?.value)) return hasEllipsisAnywhere(item.value);
+
+ return false;
+ });
+ }
+
+ return false;
+ };
+
+ const renderData = ({
+ dataItem,
+ parentId,
+ voidAsEmptyFn,
+ isFnParams,
+ }: {
+ dataItem: FormattedEventData;
+ parentId?: string;
+ voidAsEmptyFn?: boolean;
+ isFnParams?: boolean;
+ }) => {
+ const { type, value } = dataItem;
+
+ // Ellipsis (special case) for collapsed params
+ if (type === "ellipsis") {
+ return {value};
+ }
+
+ // Array
+ if (type === "vec") {
+ let renderVal = value;
+
+ if (isFnParams && isCollapsedView) {
+ renderVal = truncateParams(value, 4);
+ }
+
+ const hasEllipsis = hasEllipsisAnywhere(renderVal);
+
+ return (
+
+ {/* Don’t show square brackets for param arrays */}
+ {parentId ? : null}
+
+ {renderVal.map((v, vIndex) => {
+ const id = `vec-${vIndex}`;
+
+ return (
+
+ {renderData({ dataItem: v, parentId: id })}
+
+
+ );
+ })}
+
+ {/* We need to hide extra brackets if there is ellipsis */}
+ {parentId && !hasEllipsis ? : null}
+
+ );
+ }
+
+ // Object
+ if (type === "map") {
+ return (
+
+
+ {value.map((v, vIndex) => {
+ return (
+
+ {renderData({ dataItem: v.key })}
+
+ {renderData({ dataItem: v.val })}
+
+
+ );
+ })}
+ {!hasEllipsisAnywhere(value.map((item) => item.val)) && (
+
+ )}
+
+ );
+ }
+
+ // Void
+ if (type === "void") {
+ // For function params, we want to show void as ()
+ return ;
+ }
+
+ if (type === "address") {
+ return ;
+ }
+
+ // Primitive
+ return (
+
+ );
+ };
+
+ const renderContractId = (contractId: string) => (
+
+ at
+
+ {shortenStellarAddress(contractId)}
+
+
+ );
+
+ const renderItemContent = (event: ProcessedEvent) => {
+ // Contract event
+ if (event.type === "contract_event") {
+ return (
+
+
+
+
+ {event.name}
+
+ {event.dataContractParams?.length ? (
+ <>
+
+
+ {renderData({
+ dataItem: {
+ type: "vec",
+ value: event.dataContractParams,
+ },
+ voidAsEmptyFn: true,
+ isFnParams: true,
+ })}
+
+
+ >
+ ) : null}
+
+
+ {event.contractId ? renderContractId(event.contractId) : null}
+
+
+
+ data:
+ {renderData({
+ dataItem: event.data,
+ voidAsEmptyFn: true,
+ parentId: `data-${event.name}`,
+ })}
+
+
+ );
+ }
+
+ // Function
+ return (
+
+
+
+ {event.name}
+
+
+ {renderData({
+ dataItem: event.data,
+ voidAsEmptyFn: true,
+ isFnParams: true,
+ })}
+
+
+
+
+ {event.contractId ? renderContractId(event.contractId) : null}
+
+
+ {event.return ? (
+
+
+
+
+ {renderData({
+ dataItem: event.return.data,
+ parentId: event.name,
+ })}
+ {event.return?.contractId
+ ? renderContractId(event.return.contractId)
+ : null}
+
+ ) : null}
+
+ );
+ };
+
+ const renderNested = (
+ events: ProcessedEvent[],
+ parentId?: string,
+ depth: number = 0,
+ ) => {
+ return events.map((event, eventIndex) => {
+ return (
+
+
+
+ );
+ });
+ };
+
+ return (
+
+
+
+ This log shows all contract-related events that occurred during the
+ transaction in chronological order.
+
+
+
+
+
+ setIsCollapsedView(!isCollapsedView)}
+ />
+
+
+
+ {data.errorLevel === "all" ? (
+
+ This transaction failed and was fully rolled back. All state changes
+ were reverted, and no contract effects were applied.
+
+ ) : null}
+
+ {data.errorLevel === "some" ? (
+
+ This transaction succeeded, but some internal contract calls failed
+ and their effects were rolled back.
+
+ ) : null}
+
+
+
+ {renderNested(data.callStack)}
+
+
+
+ );
+};
+
+// =============================================================================
+// Helpers
+// =============================================================================
+const isAsset = (value: unknown) => {
+ return typeof value === "string" && value?.split(":")?.length === 2;
+};
+
+const renderAssetString = (value: string) => {
+ if (isAsset(value)) {
+ const [code, issuer] = value.split(":");
+
+ return `${code}:${shortenStellarAddress(issuer)}`;
+ }
+
+ return value;
+};
+
+// =============================================================================
+// Components
+// =============================================================================
+const TypedValueItem = ({
+ value,
+ type,
+ title,
+}: {
+ value: any;
+ type?: string;
+ title?: string;
+}) => {
+ const val =
+ type && ["string", "symbol"].includes(type) ? (
+ {renderAssetString(value)}
+ ) : (
+ `${value}`
+ );
+
+ return (
+
+ {val}
+
+ );
+};
+
+const AddressItem = ({
+ value,
+ networkId,
+}: {
+ value: any;
+ networkId: NetworkType;
+}) => {
+ const seNetwork = getStellarExpertNetwork(networkId);
+ const isContract = !getContractIdError(value);
+
+ let linkEl: string | JSX.Element;
+
+ // Stellar account on Futurnet or Custom network
+ if (!isContract && !seNetwork) {
+ linkEl = shortenStellarAddress(value);
+ } else {
+ const props = {
+ href: isContract
+ ? buildContractExplorerHref(value)
+ : `${STELLAR_EXPERT}/${seNetwork}/account/${value}`,
+ ...(isContract ? { target: "_blank" } : {}),
+ };
+
+ linkEl = (
+
+ {shortenStellarAddress(value)}
+
+ );
+ }
+
+ return (
+
+ {linkEl}
+
+ );
+};
+
+const Comma = ({ enabled }: { enabled?: boolean }) => {
+ if (enabled) {
+ return ,;
+ }
+
+ return null;
+};
+
+const Colon = () => {
+ return :;
+};
+
+const Bracket = ({
+ char,
+ hasComma,
+}: {
+ char: "{" | "}" | "[" | "]" | "(" | ")";
+ hasComma?: boolean;
+}) => (
+
+ {char}
+ {hasComma ? : null}
+
+);
+
+const Quotes = ({ children }: { children: React.ReactNode }) => (
+ <>
+ {'"'}
+ {children}
+ {'"'}
+ >
+);
+
+const EventItem = ({
+ event,
+ depth,
+ renderContent,
+ renderNested,
+}: {
+ event: ProcessedEvent;
+ depth: number;
+ renderContent: (event: ProcessedEvent) => JSX.Element;
+ renderNested: (
+ events: ProcessedEvent[],
+ parentId?: string,
+ depth?: number,
+ ) => JSX.Element[];
+}) => {
+ const [isExpanded, setIsExpanded] = useState(true);
+ const hasNestedItems = event.nested.length > 0;
+
+ const getVerticalLineTop = () => {
+ const basePoint = depth === 1 ? 16 : 0;
+
+ return hasNestedItems ? basePoint + 26 : basePoint;
+ };
+
+ const getVerticalLineHeight = () => {
+ const basePoint = depth === 1 ? 16 : 0;
+
+ return basePoint + verticalLineTop;
+ };
+
+ const verticalLineTop = getVerticalLineTop();
+
+ return (
+ <>
+
+ {hasNestedItems && isExpanded ? (
+
+ ) : null}
+
+ {/* Don’t render arrow if it’s a top-level item without nested items */}
+ {depth === 0 && !hasNestedItems ? null : (
+
setIsExpanded(!isExpanded)}
+ >
+
+
+ )}
+
+ {event.isError ? (
+
+
+
+ ) : null}
+
+
+ {event.type}
+
+
+ {renderContent(event)}
+
+
+ {event.nested.length && isExpanded ? (
+
+ {renderNested(event.nested, undefined, depth + 1)}
+
+ ) : null}
+ >
+ );
+};
diff --git a/src/app/(sidebar)/transaction-dashboard/page.tsx b/src/app/(sidebar)/transaction-dashboard/page.tsx
index a7f6ea142..e032ac221 100644
--- a/src/app/(sidebar)/transaction-dashboard/page.tsx
+++ b/src/app/(sidebar)/transaction-dashboard/page.tsx
@@ -43,6 +43,7 @@ import { TokenSummary } from "./components/TokenSummary";
import { Contracts } from "./components/Contracts";
import { ResourceProfiler } from "./components/ResourceProfiler";
import { ClassicOperations } from "./components/ClassicOperations";
+import { CallStackTrace } from "./components/CallStackTrace";
import "./styles.scss";
@@ -54,7 +55,8 @@ export default function TransactionDashboard() {
| "tx-state-change"
| "tx-resource-profiler"
| "tx-signatures"
- | "tx-fee-breakdown";
+ | "tx-fee-breakdown"
+ | "tx-call-stack-trace";
const { network, txDashboard } = useStore();
@@ -388,7 +390,7 @@ export default function TransactionDashboard() {
,
isDisabled: !isDataLoaded,
}}
@@ -405,8 +407,18 @@ export default function TransactionDashboard() {
isDisabled: !isDataLoaded,
}}
tab4={{
+ id: "tx-call-stack-trace",
+ label: "Call stack trace",
+ content: (
+
+ ),
+ isDisabled: !isDataLoaded,
+ }}
+ tab5={{
id: "tx-state-change",
- label: "State Change",
+ label: "State change",
content: isDataLoaded ? (
) : (
@@ -414,21 +426,21 @@ export default function TransactionDashboard() {
),
isDisabled: !isDataLoaded,
}}
- tab5={{
+ tab6={{
id: "tx-resource-profiler",
- label: "Resource Profiler",
+ label: "Resource profiler",
content: ,
isDisabled: !isDataLoaded,
}}
- tab6={{
+ tab7={{
id: "tx-signatures",
label: "Signatures",
content: ,
isDisabled: !isDataLoaded,
}}
- tab7={{
+ tab8={{
id: "tx-fee-breakdown",
- label: "Fee Breakdown",
+ label: "Fee breakdown",
content: ,
isDisabled: !isDataLoaded,
}}
diff --git a/src/app/(sidebar)/transaction-dashboard/styles.scss b/src/app/(sidebar)/transaction-dashboard/styles.scss
index 0bd9dcc28..613eb2a85 100644
--- a/src/app/(sidebar)/transaction-dashboard/styles.scss
+++ b/src/app/(sidebar)/transaction-dashboard/styles.scss
@@ -543,3 +543,359 @@
}
}
}
+
+// =============================================================================
+// Call Stack Trace
+// =============================================================================
+.CallStackTrace {
+ border: 1px solid var(--sds-clr-gray-06);
+ padding: pxToRem(24px);
+ border-radius: pxToRem(8px);
+
+ font-family: var(--sds-ff-monospace);
+ font-size: pxToRem(14px);
+ line-height: pxToRem(18px);
+ letter-spacing: -0.28px;
+ overflow: hidden;
+
+ &__scrollable {
+ overflow: auto;
+ }
+
+ // All failed: Red background on level 1 items for failed transaction
+ &[data-error-level="all"] {
+ .CallStackTrace__event__nested {
+ & > .CallStackTrace__event {
+ &[data-is-error="true"] {
+ background-color: var(--sds-clr-red-05) !important;
+ }
+ }
+ }
+ }
+
+ // Some failed: Make labels red, no background change
+ &[data-error-level="some"] {
+ .CallStackTrace__event {
+ &[data-is-error="true"] {
+ .CallStackTrace__itemType {
+ --CallStackTrace--itemType-bg-color: #{var(--sds-clr-red-02)};
+ --CallStackTrace--itemType-border-color: #{var(--sds-clr-red-06)};
+ --CallStackTrace--itemType-text-color: #{var(--sds-clr-red-11)};
+ }
+ }
+ }
+ }
+
+ &__event {
+ display: flex;
+ flex-direction: column;
+ gap: pxToRem(8px);
+ min-width: fit-content;
+ position: relative;
+ overflow-y: hidden;
+
+ &__info {
+ display: flex;
+ flex-direction: row;
+ gap: pxToRem(4px);
+
+ .CallStackTrace__icon {
+ // Match the actual line height (with type offsets)
+ height: pxToRem(22px);
+
+ svg {
+ stroke: var(--sds-clr-gray-09);
+ display: none;
+ }
+
+ &[data-visible="true"] {
+ cursor: pointer;
+
+ svg {
+ display: block;
+ }
+ }
+ }
+ }
+
+ &[data-is-error="true"] {
+ & > .CallStackTrace__event__info {
+ border-radius: pxToRem(4px);
+ padding-top: pxToRem(2px);
+ padding-bottom: pxToRem(2px);
+ }
+ }
+
+ &__nested {
+ display: flex;
+ gap: pxToRem(8px);
+ flex-direction: column;
+
+ // Level 1 items
+ & > .CallStackTrace__event {
+ background-color: var(--sds-clr-gray-03);
+ border-radius: pxToRem(4px);
+ padding: pxToRem(16px);
+ padding-left: pxToRem(20px);
+ margin-left: pxToRem(26px);
+
+ // Level 2 and deeper items
+ .CallStackTrace__event__nested > .CallStackTrace__event {
+ padding-top: pxToRem(2px);
+ padding-bottom: pxToRem(2px);
+ padding-right: 0;
+ margin-left: pxToRem(8px);
+ }
+ }
+ }
+ }
+
+ &__verticalLine {
+ width: 1px;
+ background-color: var(--sds-clr-gray-08);
+ position: absolute;
+ // top, left, and height are set in JS
+ }
+
+ &__itemType {
+ --CallStackTrace--itemType-bg-color: #{var(--sds-clr-teal-03)};
+ --CallStackTrace--itemType-border-color: #{var(--sds-clr-teal-06)};
+ --CallStackTrace--itemType-text-color: #{var(--sds-clr-teal-11)};
+
+ border: 1px solid var(--CallStackTrace--itemType-border-color);
+ color: var(--CallStackTrace--itemType-text-color);
+ background-color: var(--CallStackTrace--itemType-bg-color);
+
+ padding: pxToRem(2px) pxToRem(6px);
+ border-radius: pxToRem(6px);
+ font-size: pxToRem(12px);
+ line-height: pxToRem(16px);
+ font-weight: var(--sds-fw-semi-bold);
+ height: fit-content;
+
+ &[data-type="contract_event"] {
+ --CallStackTrace--itemType-bg-color: #{var(--sds-clr-lilac-03)};
+ --CallStackTrace--itemType-border-color: #{var(--sds-clr-lilac-06)};
+ --CallStackTrace--itemType-text-color: #{var(--sds-clr-lilac-11)};
+ }
+ }
+
+ &__itemContent {
+ display: flex;
+ flex-wrap: wrap;
+ }
+
+ &__itemFunc {
+ &__func,
+ .CallStackTrace__bracket {
+ color: var(--sds-clr-gray-12);
+ font-weight: var(--sds-fw-bold);
+ }
+
+ &__params {
+ padding: 0 pxToRem(4px);
+
+ .CallStackTrace__comma {
+ padding-right: pxToRem(4px);
+ }
+ }
+ }
+
+ &__itemContract {
+ display: inline-flex;
+ gap: pxToRem(4px);
+ padding-left: pxToRem(4px);
+ height: pxToRem(18px);
+ }
+
+ &__itemData {
+ display: inline;
+ flex-basis: 100%;
+ color: var(--sds-clr-gray-12);
+ padding-top: pxToRem(4px);
+
+ & > span:first-child {
+ padding-right: pxToRem(4px);
+ }
+ }
+
+ &__itemReturn {
+ padding-left: pxToRem(4px);
+ display: inline-flex;
+ gap: pxToRem(4px);
+
+ .CallStackTrace__icon {
+ height: pxToRem(18px);
+
+ svg {
+ stroke: var(--sds-clr-teal-08);
+ display: block;
+ }
+ }
+
+ .CallStackTrace__comma {
+ padding-right: pxToRem(4px);
+ }
+ }
+
+ &__itemError {
+ padding-right: pxToRem(4px);
+
+ &.CallStackTrace__icon {
+ svg {
+ display: block;
+ stroke: var(--sds-clr-red-09);
+ }
+ }
+ }
+
+ &__item {
+ display: inline;
+ vertical-align: top;
+ }
+
+ &__itemObject {
+ display: inline;
+ vertical-align: top;
+
+ &__item {
+ display: inline;
+ }
+
+ // Bracket space
+ .CallStackTrace__bracket + .CallStackTrace__itemObject__item,
+ .CallStackTrace__itemObject__item + .CallStackTrace__bracket {
+ padding-left: pxToRem(4px);
+ }
+ }
+
+ &__itemArray {
+ display: inline;
+ vertical-align: top;
+
+ &__items {
+ display: inline;
+ }
+
+ // Bracket space
+ .CallStackTrace__bracket + .CallStackTrace__itemArray__items:not(:empty) {
+ padding-left: pxToRem(4px);
+ padding-right: pxToRem(4px);
+
+ // Remove padding if there is an ellipsis
+ &[data-has-ellipsis="true"] {
+ padding-right: 0;
+ }
+ }
+
+ &__item {
+ display: inline;
+ vertical-align: top;
+ }
+ }
+
+ &__typedValueItem {
+ white-space: nowrap;
+ color: var(--sds-clr-gray-12);
+
+ &[data-type] {
+ display: inline-flex;
+
+ &::after {
+ display: block;
+ content: attr(data-type);
+ margin-top: pxToRem(6px);
+ padding-left: pxToRem(2px);
+
+ white-space: nowrap;
+ font-size: pxToRem(12px);
+ line-height: pxToRem(16px);
+ color: var(--sds-clr-gray-11);
+ }
+ }
+
+ // Colors
+ // Symbol
+ &[data-type="sym"] {
+ color: var(--sds-clr-navy-10);
+ }
+
+ // String
+ &[data-type="string"] {
+ color: var(--sds-clr-gray-12);
+ }
+
+ // Bytes
+ &[data-type="bytes"],
+ &[data-type="bytesN"] {
+ color: var(--sds-clr-gray-10);
+ }
+
+ // Number
+ &[data-type="u8"],
+ &[data-type="u16"],
+ &[data-type="u32"],
+ &[data-type="u64"],
+ &[data-type="i8"],
+ &[data-type="i16"],
+ &[data-type="i32"],
+ &[data-type="i64"] {
+ color: var(--sds-clr-mint-08);
+ }
+
+ // Big int
+ &[data-type="i128"],
+ &[data-type="i256"],
+ &[data-type="u128"],
+ &[data-type="u256"] {
+ color: var(--sds-clr-teal-08);
+ }
+
+ // Bool
+ &[data-type="bool"] {
+ color: var(--sds-clr-pink-10);
+ }
+
+ // Address
+ &[data-type="address"] {
+ color: var(--sds-clr-gray-10);
+ }
+ }
+
+ &__bracket,
+ &__comma,
+ &__colon,
+ &__ellipsis {
+ color: var(--sds-clr-gray-12);
+ }
+
+ &__bracket {
+ font-weight: var(--sds-fw-bold);
+ }
+
+ &__colon {
+ margin-right: pxToRem(4px);
+ }
+
+ &__icon {
+ flex-shrink: 0;
+ display: inline-flex;
+ justify-content: center;
+ align-items: center;
+ width: pxToRem(16px);
+ height: pxToRem(18px);
+
+ svg {
+ transition: transform ease-in-out 0.2s;
+ transform: rotate(0deg);
+ width: 100%;
+ height: 100%;
+ }
+
+ &[data-is-expanded="false"] {
+ svg {
+ transform: rotate(-90deg);
+ }
+ }
+ }
+}
diff --git a/src/components/TabView/index.tsx b/src/components/TabView/index.tsx
index ccc6edd5e..357c4655f 100644
--- a/src/components/TabView/index.tsx
+++ b/src/components/TabView/index.tsx
@@ -23,6 +23,9 @@ type TabViewProps = {
tab5?: Tab;
tab6?: Tab;
tab7?: Tab;
+ tab8?: Tab;
+ tab9?: Tab;
+ tab10?: Tab;
onTabChange: (id: string) => void;
activeTabId: string;
rightElement?: React.ReactNode;
diff --git a/src/components/Tabs/styles.scss b/src/components/Tabs/styles.scss
index f4a5280a8..65615db4a 100644
--- a/src/components/Tabs/styles.scss
+++ b/src/components/Tabs/styles.scss
@@ -6,14 +6,15 @@
display: flex;
align-items: center;
- gap: pxToRem(8px);
+ gap: pxToRem(4px);
flex-wrap: wrap;
+ padding: pxToRem(6px) 0;
.Tab {
position: relative;
font-size: pxToRem(14px);
line-height: pxToRem(20px);
- font-weight: var(--sds-fw-medium);
+ font-weight: var(--sds-fw-semi-bold);
white-space: nowrap;
color: var(--Tabs-default-text);
background-color: var(--Tabs-default-background);
diff --git a/src/constants/networkLimits.ts b/src/constants/networkLimits.ts
index 1a5a697c8..c1f7f542c 100644
--- a/src/constants/networkLimits.ts
+++ b/src/constants/networkLimits.ts
@@ -78,36 +78,36 @@ export const MAINNET_LIMITS: NetworkLimits = {
"persistent_rent_rate_denominator": "1215",
"temp_rent_rate_denominator": "2430",
"live_soroban_state_size_window": [
- "759949737",
- "760533009",
- "760981473",
- "761873993",
- "762654761",
- "763211913",
- "763986321",
- "764372305",
- "765310973",
- "766353473",
- "766526733",
- "767200145",
- "767862469",
- "768736417",
- "769079513",
- "769346157",
- "769900365",
- "770671501",
- "771407257",
- "771707833",
- "772057009",
- "772711109",
- "773748293",
- "774298277",
- "774596885",
- "774964765",
- "775748333",
- "776769589",
- "777132513",
- "777472237"
+ "734987214",
+ "735395346",
+ "736292614",
+ "737094086",
+ "737381086",
+ "737995950",
+ "738818622",
+ "739372270",
+ "739950126",
+ "739779302",
+ "740500630",
+ "741589278",
+ "742031213",
+ "742814137",
+ "743101929",
+ "743393749",
+ "744321737",
+ "744617157",
+ "745035633",
+ "745157193",
+ "745977605",
+ "746877909",
+ "746900665",
+ "747337629",
+ "747627725",
+ "748413573",
+ "748909625",
+ "749249961",
+ "749541173",
+ "749725505"
],
"state_target_size_bytes": "3000000000",
"rent_fee_1kb_state_size_low": "-17000",
@@ -146,36 +146,36 @@ export const TESTNET_LIMITS: NetworkLimits = {
"persistent_rent_rate_denominator": "1215",
"temp_rent_rate_denominator": "2430",
"live_soroban_state_size_window": [
- "513740736",
- "513758204",
- "514444783",
- "514455423",
- "514471523",
- "514459011",
- "514425827",
- "508299283",
- "509220104",
- "509238804",
- "509246772",
- "509285549",
- "508842657",
- "508859077",
- "510177526",
- "510129282",
- "506441710",
- "506461146",
- "506468034",
- "506472790",
- "506476438",
- "506484658",
- "506486030",
- "506512246",
- "506447318",
- "506462382",
- "506652376",
- "507206030",
- "507211734",
- "507222702"
+ "517984233",
+ "518608717",
+ "517961834",
+ "517967810",
+ "517974038",
+ "517989562",
+ "517477838",
+ "517991097",
+ "518694029",
+ "519561095",
+ "520305875",
+ "520949604",
+ "521559027",
+ "522340228",
+ "523407390",
+ "524986607",
+ "525947964",
+ "526820560",
+ "527579212",
+ "528572058",
+ "529394401",
+ "529648481",
+ "530473809",
+ "530900493",
+ "530907849",
+ "531092996",
+ "531750170",
+ "531742446",
+ "532135555",
+ "532434588"
],
"state_target_size_bytes": "3000000000",
"rent_fee_1kb_state_size_low": "-17000",
@@ -214,36 +214,36 @@ export const FUTURENET_LIMITS: NetworkLimits = {
"persistent_rent_rate_denominator": "1215",
"temp_rent_rate_denominator": "2430",
"live_soroban_state_size_window": [
- "49213232",
- "49213232",
- "49213232",
- "49213232",
- "49213232",
- "49213232",
- "49213232",
- "49213232",
- "49213232",
- "49213232",
- "49213232",
- "49213232",
- "49213232",
- "49213232",
- "49213232",
- "49213232",
- "49213232",
- "49213232",
- "49213232",
- "49213232",
- "49213232",
- "49213232",
- "49213232",
- "49213232",
- "49213232",
- "49213232",
- "49213232",
- "49213232",
- "49213232",
- "49213232"
+ "49939481",
+ "49939481",
+ "49939481",
+ "49939481",
+ "49939481",
+ "49939481",
+ "49939481",
+ "49939481",
+ "49939481",
+ "49939481",
+ "49939481",
+ "49939481",
+ "49939481",
+ "49939481",
+ "49939481",
+ "49939481",
+ "49939481",
+ "49939481",
+ "49939481",
+ "49939481",
+ "49939481",
+ "49939481",
+ "49939481",
+ "49939481",
+ "49939481",
+ "49939481",
+ "49939481",
+ "49939481",
+ "49939481",
+ "49939481"
],
"state_target_size_bytes": "3000000000",
"rent_fee_1kb_state_size_low": "-17000",
diff --git a/src/helpers/formatDiagnosticEvents.ts b/src/helpers/formatDiagnosticEvents.ts
new file mode 100644
index 000000000..a080de02c
--- /dev/null
+++ b/src/helpers/formatDiagnosticEvents.ts
@@ -0,0 +1,401 @@
+// =============================================================================
+// Constants
+// =============================================================================
+const EVENT_TOPIC_SYMBOLS = {
+ FN_CALL: "fn_call",
+ FN_RETURN: "fn_return",
+ CORE_METRICS: "core_metrics",
+} as const;
+
+const EVENT_TYPES = {
+ DIAGNOSTIC: "diagnostic",
+ CONTRACT: "contract",
+} as const;
+
+// =============================================================================
+// Type Definitions
+// =============================================================================
+export type ErrorLevel = "all" | "some" | undefined;
+export type EventTopicSymbol =
+ (typeof EVENT_TOPIC_SYMBOLS)[keyof typeof EVENT_TOPIC_SYMBOLS];
+export type EventType = (typeof EVENT_TYPES)[keyof typeof EVENT_TYPES];
+
+type ScError =
+ | { contract: number }
+ | { wasm_vm: unknown }
+ | { context: unknown }
+ | { storage: unknown }
+ | { object: unknown }
+ | { crypto: unknown }
+ | { events: unknown }
+ | { budget: unknown }
+ | { value: unknown }
+ | { auth: unknown };
+
+type ScInt128 = { hi: number; lo: number };
+type ScInt256 = {
+ hi_hi: number;
+ hi_lo: number;
+ lo_hi: number;
+ lo_lo: number;
+};
+
+type ScMapEntry = { key: ScValType; val: ScValType };
+type FormattedMapEntry = { key: FormattedEventData; val: FormattedEventData };
+
+type FormattedPrimitive =
+ | { type: "symbol"; value: string }
+ | { type: "bytes"; value: string }
+ | { type: "address"; value: string }
+ | { type: "i32"; value: number }
+ | { type: "i64"; value: bigint | string }
+ | { type: "i128"; value: ScInt128 | string }
+ | { type: "i256"; value: ScInt256 }
+ | { type: "u32"; value: number }
+ | { type: "u64"; value: bigint | string }
+ | { type: "u128"; value: ScInt128 | string }
+ | { type: "u256"; value: ScInt256 }
+ | { type: "bool"; value: boolean }
+ | { type: "string"; value: string }
+ | { type: "error"; value: ScError }
+ | { type: "void"; value: null };
+
+export type FormattedEventData =
+ | FormattedPrimitive
+ | { type: "vec"; value: FormattedEventData[] }
+ | { type: "map"; value: FormattedMapEntry[] }
+ | { type: "literal"; value: string }
+ | { type: "ellipsis"; value: string }
+ | { type: undefined; value: unknown };
+
+interface CoreMetric {
+ key: string | undefined;
+ value: any;
+ type: string;
+}
+
+export interface ProcessedEvent {
+ type: string;
+ name: string;
+ contractId: string | null;
+ data: FormattedEventData;
+ dataContractParams?: FormattedEventData[];
+ isError: boolean;
+ nested: ProcessedEvent[];
+ return?: ProcessedEvent;
+}
+
+interface ScValType {
+ symbol?: string;
+ bytes?: string;
+ address?: string;
+ i32?: number;
+ i64?: bigint | string;
+ i128?: ScInt128 | string;
+ i256?: ScInt256;
+ u32?: number;
+ u64?: bigint | string;
+ u128?: ScInt128 | string;
+ u256?: ScInt256;
+ bool?: boolean;
+ string?: string;
+ vec?: ScValType[];
+ map?: ScMapEntry[];
+ error?: ScError;
+ void?: null;
+}
+
+interface DiagnosticEventBody {
+ v0: {
+ topics: ScValType[];
+ data: ScValType | string;
+ };
+}
+
+interface DiagnosticEvent {
+ ext: string;
+ contract_id: string | null;
+ type_: EventType;
+ body: DiagnosticEventBody;
+}
+
+export interface DiagnosticEventJson {
+ in_successful_contract_call: boolean;
+ event: DiagnosticEvent;
+}
+
+// =============================================================================
+// Helper Functions
+// =============================================================================
+const getEventType = (event: DiagnosticEvent): string => {
+ switch (event.type_) {
+ case EVENT_TYPES.DIAGNOSTIC:
+ return event.body?.v0?.topics[0]?.symbol || "";
+ case EVENT_TYPES.CONTRACT:
+ return "contract_event";
+ default:
+ return "";
+ }
+};
+
+const getEventName = (event: DiagnosticEvent): string => {
+ const topics = event.body?.v0?.topics;
+ if (!topics || topics.length === 0) return "";
+
+ const topicSymbol = topics[0]?.symbol;
+
+ if (event.type_ === EVENT_TYPES.DIAGNOSTIC) {
+ if (topicSymbol === EVENT_TOPIC_SYMBOLS.FN_RETURN) {
+ // Use the function name (2nd topic)
+ return topics?.[1]?.symbol || "";
+ } else if (topicSymbol === EVENT_TOPIC_SYMBOLS.FN_CALL) {
+ // Use the function name (3rd topic)
+ return topics?.[2]?.symbol || "";
+ }
+
+ return topicSymbol || "";
+ }
+
+ // No name for contract events
+ if (event.type_ === EVENT_TYPES.CONTRACT) {
+ return "";
+ }
+
+ return "";
+};
+
+type PrimitiveKey = Exclude;
+
+const isPrimitiveKey = (key: keyof ScValType): key is PrimitiveKey =>
+ key !== "vec" && key !== "map";
+
+const formatEventData = (data: ScValType | string): FormattedEventData => {
+ // Handle plain string case (e.g., "void" literal)
+ if (typeof data === "string") {
+ if (data === "void") {
+ return { value: null, type: "void" };
+ }
+
+ return { value: data, type: "literal" };
+ }
+
+ if (typeof data === "object" && data !== null) {
+ const keys = Object.keys(data) as Array;
+
+ if (keys.length === 1) {
+ const key = keys[0];
+ const value = data[key];
+
+ switch (key) {
+ // Array
+ case "vec":
+ if (Array.isArray(value)) {
+ return {
+ value: value.map((item: ScValType) => formatEventData(item)),
+ type: "vec",
+ };
+ }
+ break;
+ // Object
+ case "map":
+ if (Array.isArray(value)) {
+ return {
+ value: (value as ScMapEntry[]).map((item) => ({
+ key: formatEventData(item.key),
+ val: formatEventData(item.val),
+ })),
+ type: "map",
+ };
+ }
+ break;
+ // Primitives
+ default:
+ if (isPrimitiveKey(key) && value !== undefined) {
+ switch (key) {
+ case "symbol":
+ case "bytes":
+ case "address":
+ case "string":
+ return { value: value as string, type: key };
+ case "i32":
+ case "u32":
+ return { value: value as number, type: key };
+ case "i64":
+ case "u64":
+ return { value: value as bigint | string, type: key };
+ case "i128":
+ case "u128":
+ return { value: value as ScInt128 | string, type: key };
+ case "i256":
+ case "u256":
+ return { value: value as ScInt256, type: key };
+ case "bool":
+ return { value: value as boolean, type: key };
+ case "error":
+ return { value: value as ScError, type: key };
+ case "void":
+ return { value: null, type: "void" };
+ }
+ }
+ }
+ }
+ }
+
+ // Non-object or unrecognized case
+ return { value: data, type: undefined };
+};
+
+const calculateErrorLevel = (callStack: ProcessedEvent[]): ErrorLevel => {
+ // Recursively flattens the event tree to get all events including nested ones.
+ const getAllEvents = (events: ProcessedEvent[]): ProcessedEvent[] => {
+ const allEvents: ProcessedEvent[] = [];
+
+ events.forEach((event) => {
+ allEvents.push(event);
+
+ if (event.nested && event.nested.length > 0) {
+ allEvents.push(...getAllEvents(event.nested));
+ }
+
+ if (event.return) {
+ allEvents.push(event.return);
+ }
+ });
+
+ return allEvents;
+ };
+
+ const allEvents = getAllEvents(callStack);
+
+ if (allEvents.length === 0) return undefined;
+
+ const errorEvents = allEvents.filter((event) => event.isError === true);
+
+ if (errorEvents.length === 0) return undefined;
+ if (errorEvents.length === allEvents.length) return "all";
+
+ return "some";
+};
+
+// =============================================================================
+// Main Processing
+// =============================================================================
+const formatContractParams = (
+ eventType: string | undefined,
+ topics: ScValType[] | undefined,
+): FormattedEventData[] | undefined => {
+ if (eventType !== EVENT_TYPES.CONTRACT || !topics) {
+ return undefined;
+ }
+
+ return topics.map((topic) => formatEventData(topic));
+};
+
+const processEvents = (
+ dEvents: DiagnosticEventJson[],
+): {
+ callStack: ProcessedEvent[];
+ coreMetrics: CoreMetric[];
+} => {
+ const currentStack: ProcessedEvent[] = [];
+ const result: ProcessedEvent[] = [];
+ const coreMetrics: CoreMetric[] = [];
+
+ dEvents.forEach((ev) => {
+ const evType = ev.event?.type_;
+ const evTopics = ev.event?.body?.v0?.topics;
+ const topicSymbol = evTopics?.[0]?.symbol;
+
+ // Core metrics
+ if (topicSymbol === EVENT_TOPIC_SYMBOLS.CORE_METRICS) {
+ const dataEntries = ev.event?.body?.v0?.data
+ ? Object.entries(ev.event.body.v0.data)
+ : [];
+
+ if (dataEntries.length > 0) {
+ const [type, value] = dataEntries[0];
+ coreMetrics.push({
+ key: evTopics?.[1]?.symbol,
+ value,
+ type,
+ });
+ }
+ return;
+ }
+
+ // Diagnostic and contract events
+ if (evType !== EVENT_TYPES.DIAGNOSTIC && evType !== EVENT_TYPES.CONTRACT) {
+ return;
+ }
+
+ const eventObj: ProcessedEvent = {
+ type: getEventType(ev.event),
+ name: getEventName(ev.event),
+ contractId: ev.event?.contract_id || null,
+ data: formatEventData(ev.event?.body?.v0?.data),
+ dataContractParams: formatContractParams(evType, evTopics),
+ isError: !ev.in_successful_contract_call,
+ nested: [],
+ };
+
+ const addEvent = (event: ProcessedEvent): void => {
+ if (currentStack.length > 0) {
+ currentStack[currentStack.length - 1].nested.push(event);
+ } else {
+ result.push(event);
+ }
+ };
+
+ // Process event based on type and add to appropriate location in call stack
+ if (evType === EVENT_TYPES.DIAGNOSTIC) {
+ if (topicSymbol === EVENT_TOPIC_SYMBOLS.FN_CALL) {
+ // Add to parent/result and push to current stack
+ addEvent(eventObj);
+ currentStack.push(eventObj);
+ } else if (topicSymbol === EVENT_TOPIC_SYMBOLS.FN_RETURN) {
+ // Add to current function's return property and remove from current stack
+ if (currentStack.length > 0) {
+ currentStack[currentStack.length - 1].return = eventObj;
+ currentStack.pop();
+ } else {
+ // Edge case: fn_return without matching fn_call - add to root
+ result.push(eventObj);
+ }
+ } else {
+ // Add to current stack
+ addEvent(eventObj);
+ }
+ } else if (evType === EVENT_TYPES.CONTRACT) {
+ // Add to current stack
+ addEvent(eventObj);
+ }
+ });
+
+ return { callStack: result, coreMetrics };
+};
+
+export const formatDiagnosticEvents = (
+ dEvents: DiagnosticEventJson[],
+): {
+ callStack: ProcessedEvent[];
+ // In case we want to include core metrics in the future
+ // coreMetrics: CoreMetric[];
+ errorLevel: ErrorLevel;
+} => {
+ // Early return for empty input
+ if (!dEvents || dEvents.length === 0) {
+ return {
+ callStack: [],
+ // coreMetrics: [],
+ errorLevel: undefined,
+ };
+ }
+
+ const { callStack } = processEvents(dEvents);
+
+ return {
+ callStack,
+ // coreMetrics,
+ errorLevel: calculateErrorLevel(callStack),
+ };
+};
diff --git a/src/types/types.ts b/src/types/types.ts
index 7ce4acebd..856179f7e 100644
--- a/src/types/types.ts
+++ b/src/types/types.ts
@@ -2,6 +2,7 @@ import React from "react";
import { NetworkError, rpc as StellarRpc, xdr } from "@stellar/stellar-sdk";
import type { JSONSchema7 } from "json-schema";
import { TransactionBuildParams } from "@/store/createStore";
+import type { DiagnosticEventJson } from "@/helpers/formatDiagnosticEvents";
// =============================================================================
// Generic
@@ -598,7 +599,7 @@ export type RpcTxJsonResponse = {
envelopeJson: AnyObject;
resultJson: AnyObject;
resultMetaJson: AnyObject;
- diagnosticEventsJson: AnyObject;
+ diagnosticEventsJson: DiagnosticEventJson[] | AnyObject | undefined;
};
// =============================================================================