Skip to content
Original file line number Diff line number Diff line change
@@ -1,14 +1,130 @@
import { loginRedirect } from "@app/login/loginRedirect";
import { notFound, redirect } from "next/navigation";
import { getContract } from "thirdweb";
import { defineChain } from "thirdweb/chains";
import { getCompilerMetadata } from "thirdweb/contract";
import { decodeFunctionData, toFunctionSelector } from "thirdweb/utils";
import type { AbiFunction } from "abitype";
import { getAuthToken } from "@/api/auth-token";
import { getProject } from "@/api/projects";
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import { serverThirdwebClient } from "@/constants/thirdweb-client.server";
import {
getSingleTransaction,
getTransactionActivityLogs,
} from "../../lib/analytics";
import type { Transaction } from "../../analytics/tx-table/types";
import { TransactionDetailsUI } from "./transaction-details-ui";

type AbiItem =
| AbiFunction
| {
type: string;
name?: string;
};

export type DecodedTransactionData = {
contractName: string;
functionName: string;
functionArgs: Record<string, unknown>;
} | null;

async function decodeTransactionData(
transaction: Transaction,
): Promise<DecodedTransactionData> {
try {
// Check if we have transaction parameters
if (
!transaction.transactionParams ||
transaction.transactionParams.length === 0
) {
return null;
}

// Get the first transaction parameter (assuming single transaction)
const txParam = transaction.transactionParams[0];
if (!txParam || !txParam.to || !txParam.data) {
return null;
}

// Ensure we have a chainId
if (!transaction.chainId) {
return null;
}

const chainId = parseInt(transaction.chainId);

// Create contract instance
const contract = getContract({
client: serverThirdwebClient,
address: txParam.to,
chain: defineChain(chainId),
});

// Fetch compiler metadata
const compilerMetadata = await getCompilerMetadata(contract);

if (!compilerMetadata || !compilerMetadata.abi) {
return null;
}

const contractName = compilerMetadata.name || "Unknown Contract";
const abi = compilerMetadata.abi;

// Extract function selector from transaction data (first 4 bytes)
const functionSelector = txParam.data.slice(0, 10) as `0x${string}`;

// Find matching function in ABI
const functions = (abi as readonly AbiItem[]).filter(
(item): item is AbiFunction => item.type === "function",
);
let matchingFunction: AbiFunction | null = null;

for (const func of functions) {
const selector = toFunctionSelector(func);
if (selector === functionSelector) {
matchingFunction = func;
break;
}
}

if (!matchingFunction) {
return null;
}

const functionName = matchingFunction.name;

// Decode function data
const decodedData = (await decodeFunctionData({
contract: getContract({
...contract,
abi: [matchingFunction],
}),
data: txParam.data,
})) as { args: readonly unknown[] };

// Create a clean object for display
const functionArgs: Record<string, unknown> = {};
if (matchingFunction.inputs && decodedData.args) {
for (let index = 0; index < matchingFunction.inputs.length; index++) {
const input = matchingFunction.inputs[index];
if (input) {
functionArgs[input.name || `arg${index}`] = decodedData.args[index];
}
}
}

return {
contractName,
functionName,
functionArgs,
};
} catch (error) {
console.error("Error decoding transaction:", error);
return null;
}
}

export default async function TransactionPage({
params,
}: {
Expand Down Expand Up @@ -51,6 +167,9 @@ export default async function TransactionPage({
notFound();
}

// Decode transaction data on the server
const decodedTransactionData = await decodeTransactionData(transactionData);

return (
<div className="space-y-6 p-2">
<TransactionDetailsUI
Expand All @@ -59,6 +178,7 @@ export default async function TransactionPage({
project={project}
teamSlug={team_slug}
transaction={transactionData}
decodedTransactionData={decodedTransactionData}
/>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,23 +17,27 @@ import { Button } from "@/components/ui/button";
import { CopyTextButton } from "@/components/ui/CopyTextButton";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { CodeClient } from "@/components/ui/code/code.client";
import { TabButtons } from "@/components/ui/tabs";
import { ToolTipLabel } from "@/components/ui/tooltip";
import { useAllChainsData } from "@/hooks/chains/allChains";
import { ChainIconClient } from "@/icons/ChainIcon";
import { statusDetails } from "../../analytics/tx-table/tx-table-ui";
import type { Transaction } from "../../analytics/tx-table/types";
import type { ActivityLogEntry } from "../../lib/analytics";
import type { DecodedTransactionData } from "./page";

export function TransactionDetailsUI({
transaction,
client,
activityLogs,
decodedTransactionData,
}: {
transaction: Transaction;
teamSlug: string;
client: ThirdwebClient;
project: Project;
activityLogs: ActivityLogEntry[];
decodedTransactionData: DecodedTransactionData;
}) {
const { idToChain } = useAllChainsData();

Expand All @@ -54,8 +58,8 @@ export function TransactionDetailsUI({
executionResult && "error" in executionResult
? executionResult.error.message
: executionResult && "revertData" in executionResult
? executionResult.revertData?.revertReason
: null;
? executionResult.revertData?.revertReason
: null;
const errorDetails =
executionResult && "error" in executionResult
? executionResult.error
Expand Down Expand Up @@ -243,24 +247,10 @@ export function TransactionDetailsUI({
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-lg">Transaction Parameters</CardTitle>
</CardHeader>
<CardContent>
{transaction.transactionParams &&
transaction.transactionParams.length > 0 ? (
<CodeClient
code={JSON.stringify(transaction.transactionParams, null, 2)}
lang="json"
/>
) : (
<p className="text-muted-foreground text-sm">
No transaction parameters available
</p>
)}
</CardContent>
</Card>
<TransactionParametersCard
transaction={transaction}
decodedTransactionData={decodedTransactionData}
/>
{errorMessage && (
<Card className="border-destructive">
<CardHeader>
Expand Down Expand Up @@ -376,6 +366,96 @@ export function TransactionDetailsUI({
);
}

// Transaction Parameters Card with Tabs
function TransactionParametersCard({
transaction,
decodedTransactionData,
}: {
transaction: Transaction;
decodedTransactionData: DecodedTransactionData;
}) {
const [activeTab, setActiveTab] = useState<"decoded" | "raw">("decoded");

return (
<Card>
<CardHeader>
<CardTitle className="text-lg">Transaction Parameters</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<TabButtons
tabClassName="!text-sm"
tabs={[
{
isActive: activeTab === "decoded",
name: "Decoded",
onClick: () => setActiveTab("decoded"),
},
{
isActive: activeTab === "raw",
name: "Raw",
onClick: () => setActiveTab("raw"),
},
]}
/>

{activeTab === "decoded" ? (
<DecodedTransactionDisplay decodedData={decodedTransactionData} />
) : (
<div>
{transaction.transactionParams &&
transaction.transactionParams.length > 0 ? (
<CodeClient
code={JSON.stringify(transaction.transactionParams, null, 2)}
lang="json"
/>
) : (
<p className="text-muted-foreground text-sm">
No transaction parameters available
</p>
)}
</div>
)}
</CardContent>
</Card>
);
}

// Client component to display decoded transaction data
function DecodedTransactionDisplay({
decodedData,
}: {
decodedData: DecodedTransactionData;
}) {
if (!decodedData) {
return (
<p className="text-muted-foreground text-sm">
Unable to decode transaction data. The contract may not have verified
metadata available.
</p>
);
}

return (
<div className="space-y-4">
<div>
<div className="text-muted-foreground text-sm">Contract Name</div>
<div className="font-mono text-sm">{decodedData.contractName}</div>
</div>
<div>
<div className="text-muted-foreground text-sm">Function Name</div>
<div className="font-mono text-sm">{decodedData.functionName}</div>
</div>
<div>
<div className="text-muted-foreground text-sm">Function Arguments</div>
<CodeClient
code={JSON.stringify(decodedData.functionArgs, null, 2)}
lang="json"
/>
</div>
</div>
);
}

// Activity Log Timeline Component
function ActivityLogCard({
activityLogs,
Expand Down
Loading