diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/tx/[id]/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/tx/[id]/page.tsx index 02ad3c9aec2..f76e1cea6e4 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/tx/[id]/page.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/tx/[id]/page.tsx @@ -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; +} | null; + +export type DecodedTransactionResult = DecodedTransactionData[]; + +async function decodeSingleTransactionParam( + txParam: { + to: string; + data: `0x${string}`; + value: string; + }, + chainId: number, +): Promise { + 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[]; + + // Create a clean object for display + const functionArgs: Record = {}; + 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 { + 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, }: { @@ -51,11 +217,15 @@ export default async function TransactionPage({ notFound(); } + // Decode transaction data on the server + const decodedTransactionData = await decodeTransactionData(transactionData); + return (
- - - Transaction Parameters - - - {transaction.transactionParams && - transaction.transactionParams.length > 0 ? ( - - ) : ( -

- No transaction parameters available -

- )} -
-
+ {errorMessage && ( @@ -271,7 +262,7 @@ export function TransactionDetailsUI({ {errorDetails ? ( ) : ( @@ -376,6 +367,171 @@ export function TransactionDetailsUI({ ); } +// Transaction Parameters Card with Tabs +function TransactionParametersCard({ + transaction, + decodedTransactionData, +}: { + transaction: Transaction; + decodedTransactionData: DecodedTransactionResult; +}) { + const [activeTab, setActiveTab] = useState<"decoded" | "raw">("decoded"); + + return ( + + + Transaction Parameters + + + setActiveTab("decoded"), + }, + { + isActive: activeTab === "raw", + name: "Raw", + onClick: () => setActiveTab("raw"), + }, + ]} + /> + + {activeTab === "decoded" ? ( + setActiveTab("raw")} + /> + ) : ( +
+ {transaction.transactionParams && + transaction.transactionParams.length > 0 ? ( + + ) : ( +

+ No transaction parameters available +

+ )} +
+ )} +
+
+ ); +} + +// Client component to display list of decoded transaction data +function DecodedTransactionListDisplay({ + decodedDataList, + onSwitchToRaw, +}: { + decodedDataList: DecodedTransactionResult; + onSwitchToRaw: () => void; +}) { + if (decodedDataList.length === 0) { + return ( +

+ Unable to decode transaction data. The contract may not have verified + metadata available.{" "} + + . +

+ ); + } + + return ( +
+ {decodedDataList.map( + (decodedData: DecodedTransactionData, index: number) => { + return ( +
+ {index > 0 &&
} + +
+ ); + }, + )} +
+ ); +} + +// Client component to display decoded transaction data +function DecodedTransactionDisplay({ + decodedData, + onSwitchToRaw, +}: { + decodedData: DecodedTransactionData; + onSwitchToRaw: () => void; +}) { + if (!decodedData) { + return ( +
+

+ Unable to decode transaction data. The contract may not have verified + metadata available.{" "} + + . +

+
+ ); + } + + return ( +
+
+
+
Target
+
+ + {decodedData.contractName} + +
+
+
+
Function
+
{decodedData.functionName}
+
+
+
Value
+
{decodedData.value}
+
+
+
+
Arguments
+ +
+
+ ); +} + // Activity Log Timeline Component function ActivityLogCard({ activityLogs,