Skip to content
Merged
7 changes: 4 additions & 3 deletions src/app/(sidebar)/transaction-dashboard/components/Events.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { Box } from "@/components/layout/Box";
import { ScValPrettyJson } from "@/components/ScValPrettyJson";
import { ExpandBox } from "@/components/ExpandBox";
import { CopyJsonPayloadButton } from "@/components/CopyJsonPayloadButton";
import { TransactionTabEmptyMessage } from "@/components/TransactionTabEmptyMessage";

import { shortenStellarAddress } from "@/helpers/shortenStellarAddress";
import { FormattedTxEvent, formatTxEvents } from "@/helpers/formatTxEvents";
Expand Down Expand Up @@ -45,9 +46,9 @@ export const Events = ({
)
) {
return (
<Text as="div" size="sm" weight="regular">
There are no events in this transaction.
</Text>
<TransactionTabEmptyMessage>
There are no events in this transaction
</TransactionTabEmptyMessage>
);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
"use client";

import { Badge, Card, Text } from "@stellar/design-system";
import { Badge, Card } from "@stellar/design-system";
import ReactDiffViewer, { DiffMethod } from "react-diff-viewer";
import { stringify } from "lossless-json";

import { TransactionTabEmptyMessage } from "@/components/TransactionTabEmptyMessage";
import { Box } from "@/components/layout/Box";
import { formatTxChangeStateItems } from "@/helpers/formatTxChangeStateItems";
import { useWindowSize } from "@/hooks/useWindowSize";
Expand All @@ -21,9 +22,9 @@ export const StateChange = ({

if (!formattedData?.length) {
return (
<Text as="div" size="sm" weight="regular">
There are no state changes in this transaction.
</Text>
<TransactionTabEmptyMessage>
There are no state changes in this transaction
</TransactionTabEmptyMessage>
);
}

Expand Down
216 changes: 216 additions & 0 deletions src/app/(sidebar)/transaction-dashboard/components/TokenSummary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
import { Fragment } from "react";
import { CopyText, Icon, IconButton, Link, Text } from "@stellar/design-system";
import { BigNumber } from "bignumber.js";

import { Box } from "@/components/layout/Box";
import { DataTable } from "@/components/DataTable";
import { TransactionTabEmptyMessage } from "@/components/TransactionTabEmptyMessage";

import { formatAmount } from "@/helpers/formatAmount";
import { shortenStellarAddress } from "@/helpers/shortenStellarAddress";
import { getStellarExpertNetwork } from "@/helpers/getStellarExpertNetwork";

import { STELLAR_EXPERT } from "@/constants/settings";
import { useStore } from "@/store/useStore";

import { RpcTxJsonResponse } from "@/types/types";

export const TokenSummary = ({
txDetails,
}: {
txDetails: RpcTxJsonResponse | null | undefined;
}) => {
type TokenItem = {
addressFrom: string;
addressTo: string;
amount: string;
assetCode: string;
assetIssuer: string;
};

type EventGroupsType = {
native: {
id: string;
title: string;
items: TokenItem[];
};
asset: {
id: string;
title: string;
items: TokenItem[];
};
};

const getGroupedTransferEvents = () => {
const formattedEvents = getTransferEvents(txDetails);

const groups: EventGroupsType = {
asset: {
id: "asset-ev",
title: "Contract token transferred",
items: [],
},
native: {
id: "native-ev",
title: "Native token (XLM) transferred",
items: [],
},
};

formattedEvents.forEach((event) => {
if (event.assetCode === "XLM" && event.assetIssuer === "native") {
groups.native.items.push(event);
} else {
groups.asset.items.push(event);
}
});

// Return only groups with items
return Object.values(groups).filter((group) => group.items.length);
};

const { network } = useStore();

const seNetwork = getStellarExpertNetwork(network.id);

const formatAssetAddress = (address: string) => {
const isContractAddress = address.startsWith("C");

return (
<Box
gap="sm"
direction="row"
align="center"
addlClassName="TransactionTokenSummary__address"
>
<Link
href={`${STELLAR_EXPERT}/${seNetwork}/${isContractAddress ? "contract" : "account"}/${address}`}
>
{shortenStellarAddress(address)}
</Link>

<CopyText textToCopy={address} tooltipPlacement="top">
<IconButton
icon={<Icon.Copy01 />}
altText="Copy address"
customSize="1rem"
/>
</CopyText>
</Box>
);
};

const formatAssetAmount = (amount: string) => {
return (
<span className="TransactionTokenSummary__amount">
{formatAmount(
new BigNumber(amount).dividedBy(10_000_000).toNumber(),
2,
)}
</span>
);
};

const formatAsset = (code: string, issuer: string) => {
if (!code || !issuer) {
return null;
}

const assetString =
code === "XLM" && issuer === "native" ? code : `${code}-${issuer}`;

return (
<Link
href={`${STELLAR_EXPERT}/${seNetwork}/asset/${assetString}`}
icon={<Icon.LinkExternal01 />}
iconPosition="right"
>
{code}
</Link>
);
};

const groupedTransferEvents = getGroupedTransferEvents();

if (!groupedTransferEvents.length) {
return (
<TransactionTabEmptyMessage>
There are no transfer events in this transaction
</TransactionTabEmptyMessage>
);
}

return (
<Box gap="lg" addlClassName="TransactionTokenSummary">
{groupedTransferEvents.map((ev) => (
<Fragment key={ev.id}>
<Box gap="md" data-testid={ev.id}>
<Text as="div" size="sm" weight="medium">
{ev.title}
</Text>

<DataTable
tableId="tx-dash-token-summary-assets"
tableData={ev.items}
tableHeaders={[
{ id: "address-from", value: "From" },
{ id: "address-to", value: "To" },
{ id: "amount", value: "Amount" },
{ id: "asset", value: "Asset" },
]}
formatDataRow={(ti: TokenItem) => [
{ value: formatAssetAddress(ti.addressFrom), isOverflow: true },
{ value: formatAssetAddress(ti.addressTo), isOverflow: true },
{ value: formatAssetAmount(ti.amount) },
{ value: formatAsset(ti.assetCode, ti.assetIssuer) },
]}
cssGridTemplateColumns="minmax(210px, 1fr) minmax(210px, 1fr) minmax(160px, 1fr) minmax(110px, 1fr)"
hidePagination={true}
/>
</Box>
</Fragment>
))}
</Box>
);
};

const getTransferEvents = (data: RpcTxJsonResponse | null | undefined) => {
const events = data?.events?.contractEventsJson;

if (!events) {
return [];
}

const contractEvents = Array.isArray(events[0])
? events.flatMap((group: any[]) => group)
: events;

return contractEvents
.filter((event: any) => event?.body?.v0?.topics?.[0]?.symbol === "transfer")
.map((ev: any) => {
const addressFrom = ev.body.v0.topics[1].address;
const addressTo = ev.body.v0.topics[2].address;

const token = ev.body.v0.topics?.[3]?.string;

let assetCode = "";
let assetIssuer = "";

if (token) {
[assetCode, assetIssuer] = token.split(":");
}

return {
addressFrom,
addressTo,
// Transfer event data type https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0041.md#transfer-event
amount:
ev.body.v0.data?.map?.[0]?.val?.i128 ||
ev.body.v0.data?.i128 ||
ev.body.v0.data?.amount ||
"0",
assetCode: assetCode === "native" ? "XLM" : assetCode,
assetIssuer: assetCode === "native" ? "native" : assetIssuer,
};
});
};
5 changes: 3 additions & 2 deletions src/app/(sidebar)/transaction-dashboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { StateChange } from "./components/StateChange";
import { FeeBreakdown } from "./components/FeeBreakdown";
import { Signatures } from "./components/Signatures";
import { Events } from "./components/Events";
import { TokenSummary } from "./components/TokenSummary";
import { Contracts } from "./components/Contracts";

import "./styles.scss";
Expand Down Expand Up @@ -313,8 +314,8 @@ export default function TransactionDashboard() {
tab1={{
id: "tx-token-summary",
label: "Token Summary",
content: <ComingSoonText />,
isDisabled: true,
content: <TokenSummary txDetails={txDetails} />,
isDisabled: !isDataLoaded,
}}
tab2={{
id: "tx-contracts",
Expand Down
40 changes: 40 additions & 0 deletions src/app/(sidebar)/transaction-dashboard/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,17 @@
}
}

.TransactionTab {
&__emptyMessage {
min-height: pxToRem(272px);
border-radius: pxToRem(8px);
background-color: var(--sds-clr-gray-03);
padding: pxToRem(16px);

color: var(--sds-clr-gray-11);
}
}

// =============================================================================
// State Change
// =============================================================================
Expand Down Expand Up @@ -408,6 +419,35 @@
}
}

// =============================================================================
// Token Summary
// =============================================================================
.TransactionTokenSummary {
.DataTable {
font-size: pxToRem(14px);

// Needed for the Copy address tooltip
&__container {
overflow: visible;
}
}

&__address {
.Link {
width: pxToRem(96px);
white-space: nowrap;
}

.CopyText {
line-height: 1;
}
}

&__amount {
color: var(--sds-clr-gray-11);
}
}

// =============================================================================
// Contracts
// =============================================================================
Expand Down
19 changes: 19 additions & 0 deletions src/components/TransactionTabEmptyMessage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Text } from "@stellar/design-system";
import { Box } from "@/components/layout/Box";

export const TransactionTabEmptyMessage = ({
children,
}: {
children: React.ReactNode;
}) => (
<Box
gap="sm"
align="center"
justify="center"
addlClassName="TransactionTab__emptyMessage"
>
<Text as="div" size="sm" weight="medium">
{children}
</Text>
</Box>
);
6 changes: 3 additions & 3 deletions src/helpers/formatAmount.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export const formatAmount = (amount: number) => {
export const formatAmount = (amount: number | bigint, precision?: number) => {
const formatter = new Intl.NumberFormat("en-US", {
minimumFractionDigits: 1,
maximumFractionDigits: 7,
minimumFractionDigits: precision || 1,
maximumFractionDigits: precision || 7,
});

return formatter.format(amount);
Expand Down
Loading