Skip to content
Original file line number Diff line number Diff line change
@@ -1,14 +1,180 @@
import { loginRedirect } from "@app/login/loginRedirect";
import type { AbiFunction } from "abitype";
import { notFound, redirect } from "next/navigation";
import { getContract, toTokens } from "thirdweb";
import { defineChain, getChainMetadata } from "thirdweb/chains";
import { getCompilerMetadata } from "thirdweb/contract";
import {
decodeFunctionData,
shortenAddress,
toFunctionSelector,
} from "thirdweb/utils";
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 type { Transaction } from "../../analytics/tx-table/types";
import {
getSingleTransaction,
getTransactionActivityLogs,
} from "../../lib/analytics";
import { TransactionDetailsUI } from "./transaction-details-ui";

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

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

export type DecodedTransactionResult = DecodedTransactionData[];

async function decodeSingleTransactionParam(
txParam: {
to: string;
data: `0x${string}`;
value: string;
},
chainId: number,
): Promise<DecodedTransactionData> {
try {
if (!txParam || !txParam.to || !txParam.data) {
return null;
}

// eslint-disable-next-line no-restricted-syntax
const chain = defineChain(chainId);

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

// Fetch compiler metadata
const chainMetadata = await getChainMetadata(chain);

const txValue = `${txParam.value ? toTokens(BigInt(txParam.value), chainMetadata.nativeCurrency.decimals) : "0"} ${chainMetadata.nativeCurrency.symbol}`;

if (txParam.data === "0x") {
return {
chainId,
contractAddress: txParam.to,
contractName: shortenAddress(txParam.to),
functionArgs: {},
functionName: "Transfer",
value: txValue,
};
}

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 decodedArgs = (await decodeFunctionData({
contract: getContract({
...contract,
abi: [matchingFunction],
}),
data: txParam.data,
})) as readonly unknown[];
Comment on lines +113 to +119
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve type safety for decoded function data.

The type assertion on the decoded function data could be made safer by validating the structure before asserting the type.

-    const decodedArgs = (await decodeFunctionData({
-      contract: getContract({
-        ...contract,
-        abi: [matchingFunction],
-      }),
-      data: txParam.data,
-    })) as readonly unknown[];
+    const decodedResult = await decodeFunctionData({
+      contract: getContract({
+        ...contract,
+        abi: [matchingFunction],
+      }),
+      data: txParam.data,
+    });
+    const decodedArgs = Array.isArray(decodedResult) ? decodedResult : [];
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const decodedArgs = (await decodeFunctionData({
contract: getContract({
...contract,
abi: [matchingFunction],
}),
data: txParam.data,
})) as readonly unknown[];
const decodedResult = await decodeFunctionData({
contract: getContract({
...contract,
abi: [matchingFunction],
}),
data: txParam.data,
});
const decodedArgs = Array.isArray(decodedResult) ? decodedResult : [];
🤖 Prompt for AI Agents
In
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/tx/[id]/page.tsx
around lines 88 to 94, the decoded function data is currently asserted with a
broad type without validation. To improve type safety, add runtime checks to
verify the structure and expected properties of the decoded data before
asserting its type. This can involve checking the data is an array and contains
expected elements, then safely casting it to the desired type.


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

return {
chainId,
contractAddress: txParam.to,
contractName,
functionArgs,
functionName,
value: txValue,
};
} catch (error) {
console.error("Error decoding transaction param:", error);
return null;
}
}

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

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

const chainId = parseInt(transaction.chainId);

// Decode all transaction parameters in parallel
const decodingPromises = transaction.transactionParams.map((txParam) =>
decodeSingleTransactionParam(txParam, chainId),
);

const results = await Promise.all(decodingPromises);
return results;
} catch (error) {
console.error("Error decoding transaction:", error);
return [];
}
}

export default async function TransactionPage({
params,
}: {
Expand Down Expand Up @@ -51,11 +217,15 @@ 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
activityLogs={activityLogs}
client={client}
decodedTransactionData={decodedTransactionData}
project={project}
teamSlug={team_slug}
transaction={transactionData}
Expand Down
Loading
Loading