From d4f3654fe52db4d0b2efb51e7e29ea6437828e62 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 5 Jul 2025 09:18:07 +0000 Subject: [PATCH 01/10] Add decoded transaction parameters with tab view in transaction details Co-authored-by: joaquim.verges --- .../tx/[id]/decoded-transaction-params.tsx | 188 ++++++++++++++++++ .../tx/[id]/transaction-details-ui.tsx | 85 ++++++-- 2 files changed, 252 insertions(+), 21 deletions(-) create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/tx/[id]/decoded-transaction-params.tsx diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/tx/[id]/decoded-transaction-params.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/tx/[id]/decoded-transaction-params.tsx new file mode 100644 index 00000000000..9b157fb8d21 --- /dev/null +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/tx/[id]/decoded-transaction-params.tsx @@ -0,0 +1,188 @@ +import { getContract } from "thirdweb"; +import { getCompilerMetadata } from "thirdweb/contracts"; +import { decodeFunctionData, toFunctionSelector } from "thirdweb/utils"; +import { CodeServer } from "@/components/ui/code/code.server"; +import { serverThirdwebClient } from "@/constants/thirdweb-client.server"; +import type { Transaction } from "../../analytics/tx-table/types"; + +type AbiFunction = { + type: "function"; + name: string; + inputs?: Array<{ + name: string; + type: string; + }>; +}; + +type AbiItem = + | AbiFunction + | { + type: string; + name?: string; + }; + +interface DecodedTransactionParamsProps { + transaction: Transaction; +} + +export async function DecodedTransactionParams({ + transaction, +}: DecodedTransactionParamsProps) { + // Check if we have transaction parameters + if ( + !transaction.transactionParams || + transaction.transactionParams.length === 0 + ) { + return ( +

+ No transaction parameters available to decode +

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

+ Transaction data is incomplete for decoding +

+ ); + } + + try { + // Create contract instance + const contract = getContract({ + client: serverThirdwebClient, + address: txParam.to, + chain: transaction.chainId + ? { id: parseInt(transaction.chainId) } + : undefined, + }); + + // Fetch compiler metadata + const compilerMetadata = await getCompilerMetadata(contract); + + if (!compilerMetadata || !compilerMetadata.abi) { + return ( +
+
+
+ Contract Address +
+
{txParam.to}
+
+
+
Error
+
+ Unable to fetch contract metadata. The contract may not have + verified metadata available. +
+
+
+ ); + } + + 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 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 ( +
+
+
Contract Name
+
{contractName}
+
+
+
+ Function Selector +
+
{functionSelector}
+
+
+
Error
+
+ No matching function found in contract ABI for selector{" "} + {functionSelector} +
+
+
+ ); + } + + const functionName = matchingFunction.name; + + // Decode function data + const decodedData = decodeFunctionData({ + abi: [matchingFunction], + data: txParam.data, + }); + + // Create a clean object for display + const functionArgs: Record = {}; + 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 ( +
+
+
Contract Name
+
{contractName}
+
+
+
Function Name
+
{functionName}
+
+
+
+ Function Arguments +
+ +
+
+ ); + } catch (error) { + console.error("Error decoding transaction:", error); + return ( +
+
+
Contract Address
+
{txParam.to}
+
+
+
Error
+
+ Failed to decode transaction data:{" "} + {error instanceof Error ? error.message : "Unknown error"} +
+
+
+ ); + } +} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/tx/[id]/transaction-details-ui.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/tx/[id]/transaction-details-ui.tsx index 8d5f2957691..22723d1aa6d 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/tx/[id]/transaction-details-ui.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/tx/[id]/transaction-details-ui.tsx @@ -8,7 +8,7 @@ import { InfoIcon, } from "lucide-react"; import Link from "next/link"; -import { useState } from "react"; +import { Suspense, useState } from "react"; import { hexToNumber, isHex, type ThirdwebClient, toEther } from "thirdweb"; import type { Project } from "@/api/projects"; import { WalletAddress } from "@/components/blocks/wallet-address"; @@ -17,12 +17,14 @@ 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 { DecodedTransactionParams } from "./decoded-transaction-params"; export function TransactionDetailsUI({ transaction, @@ -54,8 +56,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 @@ -243,24 +245,7 @@ export function TransactionDetailsUI({ - - - Transaction Parameters - - - {transaction.transactionParams && - transaction.transactionParams.length > 0 ? ( - - ) : ( -

- No transaction parameters available -

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

+ No transaction parameters available +

+ )} +
+ )} +
+
+ ); +} + // Activity Log Timeline Component function ActivityLogCard({ activityLogs, From 2570baad2b5afa69217549be9c0a8126fceb5b9e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 5 Jul 2025 09:59:14 +0000 Subject: [PATCH 02/10] Refactor transaction decoding to server-side with improved error handling Co-authored-by: joaquim.verges --- .../tx/[id]/decoded-transaction-params.tsx | 188 ------------------ .../(sidebar)/transactions/tx/[id]/page.tsx | 120 +++++++++++ .../tx/[id]/transaction-details-ui.tsx | 63 ++++-- 3 files changed, 170 insertions(+), 201 deletions(-) delete mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/tx/[id]/decoded-transaction-params.tsx diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/tx/[id]/decoded-transaction-params.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/tx/[id]/decoded-transaction-params.tsx deleted file mode 100644 index 9b157fb8d21..00000000000 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/tx/[id]/decoded-transaction-params.tsx +++ /dev/null @@ -1,188 +0,0 @@ -import { getContract } from "thirdweb"; -import { getCompilerMetadata } from "thirdweb/contracts"; -import { decodeFunctionData, toFunctionSelector } from "thirdweb/utils"; -import { CodeServer } from "@/components/ui/code/code.server"; -import { serverThirdwebClient } from "@/constants/thirdweb-client.server"; -import type { Transaction } from "../../analytics/tx-table/types"; - -type AbiFunction = { - type: "function"; - name: string; - inputs?: Array<{ - name: string; - type: string; - }>; -}; - -type AbiItem = - | AbiFunction - | { - type: string; - name?: string; - }; - -interface DecodedTransactionParamsProps { - transaction: Transaction; -} - -export async function DecodedTransactionParams({ - transaction, -}: DecodedTransactionParamsProps) { - // Check if we have transaction parameters - if ( - !transaction.transactionParams || - transaction.transactionParams.length === 0 - ) { - return ( -

- No transaction parameters available to decode -

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

- Transaction data is incomplete for decoding -

- ); - } - - try { - // Create contract instance - const contract = getContract({ - client: serverThirdwebClient, - address: txParam.to, - chain: transaction.chainId - ? { id: parseInt(transaction.chainId) } - : undefined, - }); - - // Fetch compiler metadata - const compilerMetadata = await getCompilerMetadata(contract); - - if (!compilerMetadata || !compilerMetadata.abi) { - return ( -
-
-
- Contract Address -
-
{txParam.to}
-
-
-
Error
-
- Unable to fetch contract metadata. The contract may not have - verified metadata available. -
-
-
- ); - } - - 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 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 ( -
-
-
Contract Name
-
{contractName}
-
-
-
- Function Selector -
-
{functionSelector}
-
-
-
Error
-
- No matching function found in contract ABI for selector{" "} - {functionSelector} -
-
-
- ); - } - - const functionName = matchingFunction.name; - - // Decode function data - const decodedData = decodeFunctionData({ - abi: [matchingFunction], - data: txParam.data, - }); - - // Create a clean object for display - const functionArgs: Record = {}; - 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 ( -
-
-
Contract Name
-
{contractName}
-
-
-
Function Name
-
{functionName}
-
-
-
- Function Arguments -
- -
-
- ); - } catch (error) { - console.error("Error decoding transaction:", error); - return ( -
-
-
Contract Address
-
{txParam.to}
-
-
-
Error
-
- Failed to decode transaction data:{" "} - {error instanceof Error ? error.message : "Unknown error"} -
-
-
- ); - } -} 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..3262addaf9a 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,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; +} | null; + +async function decodeTransactionData( + transaction: Transaction, +): Promise { + 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 = {}; + 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, }: { @@ -51,6 +167,9 @@ export default async function TransactionPage({ notFound(); } + // Decode transaction data on the server + const decodedTransactionData = await decodeTransactionData(transactionData); + return (
); diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/tx/[id]/transaction-details-ui.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/tx/[id]/transaction-details-ui.tsx index 22723d1aa6d..2a9f937bd6e 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/tx/[id]/transaction-details-ui.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/tx/[id]/transaction-details-ui.tsx @@ -8,7 +8,7 @@ import { InfoIcon, } from "lucide-react"; import Link from "next/link"; -import { Suspense, useState } from "react"; +import { useState } from "react"; import { hexToNumber, isHex, type ThirdwebClient, toEther } from "thirdweb"; import type { Project } from "@/api/projects"; import { WalletAddress } from "@/components/blocks/wallet-address"; @@ -24,18 +24,20 @@ 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 { DecodedTransactionParams } from "./decoded-transaction-params"; +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(); @@ -245,7 +247,10 @@ export function TransactionDetailsUI({
- + {errorMessage && ( @@ -364,7 +369,11 @@ export function TransactionDetailsUI({ // Transaction Parameters Card with Tabs function TransactionParametersCard({ transaction, -}: { transaction: Transaction }) { + decodedTransactionData, +}: { + transaction: Transaction; + decodedTransactionData: DecodedTransactionData; +}) { const [activeTab, setActiveTab] = useState<"decoded" | "raw">("decoded"); return ( @@ -390,15 +399,7 @@ function TransactionParametersCard({ /> {activeTab === "decoded" ? ( - - Decoding transaction data... - - } - > - - + ) : (
{transaction.transactionParams && @@ -419,6 +420,42 @@ function TransactionParametersCard({ ); } +// Client component to display decoded transaction data +function DecodedTransactionDisplay({ + decodedData, +}: { + decodedData: DecodedTransactionData; +}) { + if (!decodedData) { + return ( +

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

+ ); + } + + return ( +
+
+
Contract Name
+
{decodedData.contractName}
+
+
+
Function Name
+
{decodedData.functionName}
+
+
+
Function Arguments
+ +
+
+ ); +} + // Activity Log Timeline Component function ActivityLogCard({ activityLogs, From 468e6624ce9865c60cf0811adbd4e6a9b5d2a9f0 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 5 Jul 2025 10:32:17 +0000 Subject: [PATCH 03/10] Fix transaction data decoding to correctly handle function arguments Co-authored-by: joaquim.verges --- .../(sidebar)/transactions/tx/[id]/page.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 3262addaf9a..f8d10918a96 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 @@ -95,21 +95,21 @@ async function decodeTransactionData( const functionName = matchingFunction.name; // Decode function data - const decodedData = (await decodeFunctionData({ + const decodedArgs = (await decodeFunctionData({ contract: getContract({ ...contract, abi: [matchingFunction], }), data: txParam.data, - })) as { args: readonly unknown[] }; + })) as readonly unknown[]; // Create a clean object for display const functionArgs: Record = {}; - if (matchingFunction.inputs && decodedData.args) { + 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}`] = decodedData.args[index]; + functionArgs[input.name || `arg${index}`] = decodedArgs[index]; } } } From 22c30adb018660db94fb151fd19be5b7a0fc5e17 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 5 Jul 2025 20:56:27 +0000 Subject: [PATCH 04/10] Replace JSON.stringify with thirdweb stringify for better serialization Co-authored-by: joaquim.verges --- .../tx/[id]/transaction-details-ui.tsx | 1159 +++++++++-------- 1 file changed, 580 insertions(+), 579 deletions(-) diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/tx/[id]/transaction-details-ui.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/tx/[id]/transaction-details-ui.tsx index 2a9f937bd6e..35f90d9db88 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/tx/[id]/transaction-details-ui.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/tx/[id]/transaction-details-ui.tsx @@ -2,14 +2,15 @@ import { format, formatDistanceToNowStrict } from "date-fns"; import { - ChevronDownIcon, - ChevronRightIcon, - ExternalLinkIcon, - InfoIcon, + ChevronDownIcon, + ChevronRightIcon, + ExternalLinkIcon, + InfoIcon, } from "lucide-react"; import Link from "next/link"; import { useState } from "react"; import { hexToNumber, isHex, type ThirdwebClient, toEther } from "thirdweb"; +import { stringify } from "thirdweb/utils"; import type { Project } from "@/api/projects"; import { WalletAddress } from "@/components/blocks/wallet-address"; import { Badge } from "@/components/ui/badge"; @@ -27,604 +28,604 @@ import type { ActivityLogEntry } from "../../lib/analytics"; import type { DecodedTransactionData } from "./page"; export function TransactionDetailsUI({ - transaction, - client, - activityLogs, - decodedTransactionData, + transaction, + client, + activityLogs, + decodedTransactionData, }: { - transaction: Transaction; - teamSlug: string; - client: ThirdwebClient; - project: Project; - activityLogs: ActivityLogEntry[]; - decodedTransactionData: DecodedTransactionData; + transaction: Transaction; + teamSlug: string; + client: ThirdwebClient; + project: Project; + activityLogs: ActivityLogEntry[]; + decodedTransactionData: DecodedTransactionData; }) { - const { idToChain } = useAllChainsData(); - - // Extract relevant data from transaction - const { - id, - chainId, - from, - transactionHash, - confirmedAt, - createdAt, - executionParams, - executionResult, - } = transaction; - - const status = executionResult?.status as keyof typeof statusDetails; - const errorMessage = - executionResult && "error" in executionResult - ? executionResult.error.message - : executionResult && "revertData" in executionResult - ? executionResult.revertData?.revertReason - : null; - const errorDetails = - executionResult && "error" in executionResult - ? executionResult.error - : undefined; - - const chain = chainId ? idToChain.get(Number.parseInt(chainId)) : undefined; - const explorer = chain?.explorers?.[0]; - - // Calculate time difference between creation and confirmation - const confirmationTime = - confirmedAt && createdAt - ? new Date(confirmedAt).getTime() - new Date(createdAt).getTime() - : null; - - // Determine sender and signer addresses - const senderAddress = executionParams?.smartAccountAddress || from || ""; - const signerAddress = executionParams?.signerAddress || from || ""; - - // Gas information - const gasUsed = - executionResult && "actualGasUsed" in executionResult - ? `${ - isHex(executionResult.actualGasUsed) - ? hexToNumber(executionResult.actualGasUsed) - : executionResult.actualGasUsed - }` - : "N/A"; - - const gasCost = - executionResult && "actualGasCost" in executionResult - ? `${toEther(BigInt(executionResult.actualGasCost || "0"))} ${ - chain?.nativeCurrency.symbol || "" - }` - : "N/A"; - - return ( - <> -
-

- Transaction Details -

-
- -
- {/* Transaction Info Card */} - - - Transaction Information - - -
-
- Status -
-
- {status && ( - - - {statusDetails[status].name} - {errorMessage && } - - - )} -
-
- -
-
- Transaction ID -
-
- -
-
- -
-
- Transaction Hash -
-
- {transactionHash ? ( -
- {explorer ? ( - - ) : ( - - )} -
- ) : ( -
Not available yet
- )} -
-
- -
-
- Network -
-
- {chain ? ( -
- - {chain.name || "Unknown"} -
- ) : ( -
Chain ID: {chainId || "Unknown"}
- )} -
-
-
-
- - - Sender Information - - -
-
- Sender Address -
-
- -
-
- -
-
- Signer Address -
-
- -
-
-
-
- - {errorMessage && ( - - - - Error Details - - - - {errorDetails ? ( - - ) : ( -
- {errorMessage} -
- )} -
-
- )} - - - Timing Information - - -
-
- Created At -
-
- {createdAt ? ( -

- {formatDistanceToNowStrict(new Date(createdAt), { - addSuffix: true, - })}{" "} - ({format(new Date(createdAt), "PP pp z")}) -

- ) : ( - "N/A" - )} -
-
- -
-
- Confirmed At -
-
- {confirmedAt ? ( -

- {formatDistanceToNowStrict(new Date(confirmedAt), { - addSuffix: true, - })}{" "} - ({format(new Date(confirmedAt), "PP pp z")}) -

- ) : ( - "Pending" - )} -
-
- - {confirmationTime && ( -
-
- Confirmation Time -
-
- {Math.floor(confirmationTime / 1000)} seconds -
-
- )} -
-
- - - Gas Information - - -
-
- Gas Used -
-
{gasUsed}
-
- -
-
- Gas Cost -
-
{gasCost}
-
- - {transaction.confirmedAtBlockNumber && ( -
-
- Block Number -
-
- {isHex(transaction.confirmedAtBlockNumber) - ? hexToNumber(transaction.confirmedAtBlockNumber) - : transaction.confirmedAtBlockNumber} -
-
- )} -
-
- - {/* Activity Log Card */} - -
- - ); + const { idToChain } = useAllChainsData(); + + // Extract relevant data from transaction + const { + id, + chainId, + from, + transactionHash, + confirmedAt, + createdAt, + executionParams, + executionResult, + } = transaction; + + const status = executionResult?.status as keyof typeof statusDetails; + const errorMessage = + executionResult && "error" in executionResult + ? executionResult.error.message + : executionResult && "revertData" in executionResult + ? executionResult.revertData?.revertReason + : null; + const errorDetails = + executionResult && "error" in executionResult + ? executionResult.error + : undefined; + + const chain = chainId ? idToChain.get(Number.parseInt(chainId)) : undefined; + const explorer = chain?.explorers?.[0]; + + // Calculate time difference between creation and confirmation + const confirmationTime = + confirmedAt && createdAt + ? new Date(confirmedAt).getTime() - new Date(createdAt).getTime() + : null; + + // Determine sender and signer addresses + const senderAddress = executionParams?.smartAccountAddress || from || ""; + const signerAddress = executionParams?.signerAddress || from || ""; + + // Gas information + const gasUsed = + executionResult && "actualGasUsed" in executionResult + ? `${ + isHex(executionResult.actualGasUsed) + ? hexToNumber(executionResult.actualGasUsed) + : executionResult.actualGasUsed + }` + : "N/A"; + + const gasCost = + executionResult && "actualGasCost" in executionResult + ? `${toEther(BigInt(executionResult.actualGasCost || "0"))} ${ + chain?.nativeCurrency.symbol || "" + }` + : "N/A"; + + return ( + <> +
+

+ Transaction Details +

+
+ +
+ {/* Transaction Info Card */} + + + Transaction Information + + +
+
+ Status +
+
+ {status && ( + + + {statusDetails[status].name} + {errorMessage && } + + + )} +
+
+ +
+
+ Transaction ID +
+
+ +
+
+ +
+
+ Transaction Hash +
+
+ {transactionHash ? ( +
+ {explorer ? ( + + ) : ( + + )} +
+ ) : ( +
Not available yet
+ )} +
+
+ +
+
+ Network +
+
+ {chain ? ( +
+ + {chain.name || "Unknown"} +
+ ) : ( +
Chain ID: {chainId || "Unknown"}
+ )} +
+
+
+
+ + + Sender Information + + +
+
+ Sender Address +
+
+ +
+
+ +
+
+ Signer Address +
+
+ +
+
+
+
+ + {errorMessage && ( + + + + Error Details + + + + {errorDetails ? ( + + ) : ( +
+ {errorMessage} +
+ )} +
+
+ )} + + + Timing Information + + +
+
+ Created At +
+
+ {createdAt ? ( +

+ {formatDistanceToNowStrict(new Date(createdAt), { + addSuffix: true, + })}{" "} + ({format(new Date(createdAt), "PP pp z")}) +

+ ) : ( + "N/A" + )} +
+
+ +
+
+ Confirmed At +
+
+ {confirmedAt ? ( +

+ {formatDistanceToNowStrict(new Date(confirmedAt), { + addSuffix: true, + })}{" "} + ({format(new Date(confirmedAt), "PP pp z")}) +

+ ) : ( + "Pending" + )} +
+
+ + {confirmationTime && ( +
+
+ Confirmation Time +
+
+ {Math.floor(confirmationTime / 1000)} seconds +
+
+ )} +
+
+ + + Gas Information + + +
+
+ Gas Used +
+
{gasUsed}
+
+ +
+
+ Gas Cost +
+
{gasCost}
+
+ + {transaction.confirmedAtBlockNumber && ( +
+
+ Block Number +
+
+ {isHex(transaction.confirmedAtBlockNumber) + ? hexToNumber(transaction.confirmedAtBlockNumber) + : transaction.confirmedAtBlockNumber} +
+
+ )} +
+
+ + {/* Activity Log Card */} + +
+ + ); } // Transaction Parameters Card with Tabs function TransactionParametersCard({ - transaction, - decodedTransactionData, + transaction, + decodedTransactionData, }: { - transaction: Transaction; - decodedTransactionData: DecodedTransactionData; + transaction: Transaction; + decodedTransactionData: DecodedTransactionData; }) { - const [activeTab, setActiveTab] = useState<"decoded" | "raw">("decoded"); - - return ( - - - Transaction Parameters - - - setActiveTab("decoded"), - }, - { - isActive: activeTab === "raw", - name: "Raw", - onClick: () => setActiveTab("raw"), - }, - ]} - /> - - {activeTab === "decoded" ? ( - - ) : ( -
- {transaction.transactionParams && - transaction.transactionParams.length > 0 ? ( - - ) : ( -

- No transaction parameters available -

- )} -
- )} -
-
- ); + const [activeTab, setActiveTab] = useState<"decoded" | "raw">("decoded"); + + return ( + + + Transaction Parameters + + + setActiveTab("decoded"), + }, + { + isActive: activeTab === "raw", + name: "Raw", + onClick: () => setActiveTab("raw"), + }, + ]} + /> + + {activeTab === "decoded" ? ( + + ) : ( +
+ {transaction.transactionParams && + transaction.transactionParams.length > 0 ? ( + + ) : ( +

+ No transaction parameters available +

+ )} +
+ )} +
+
+ ); } // Client component to display decoded transaction data function DecodedTransactionDisplay({ - decodedData, + decodedData, }: { - decodedData: DecodedTransactionData; + decodedData: DecodedTransactionData; }) { - if (!decodedData) { - return ( -

- Unable to decode transaction data. The contract may not have verified - metadata available. -

- ); - } - - return ( -
-
-
Contract Name
-
{decodedData.contractName}
-
-
-
Function Name
-
{decodedData.functionName}
-
-
-
Function Arguments
- -
-
- ); + if (!decodedData) { + return ( +

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

+ ); + } + + return ( +
+
+
Contract Name
+
{decodedData.contractName}
+
+
+
Function Name
+
{decodedData.functionName}
+
+
+
Function Arguments
+ +
+
+ ); } // Activity Log Timeline Component function ActivityLogCard({ - activityLogs, + activityLogs, }: { - activityLogs: ActivityLogEntry[]; + activityLogs: ActivityLogEntry[]; }) { - // Sort activity logs and prepare JSX elements using for...of loop - const renderActivityLogs = () => { - if (activityLogs.length === 0) { - return ( -

- No activity logs available for this transaction -

- ); - } - - // Sort logs chronologically using for...of loop (manual sorting) - const sortedLogs: ActivityLogEntry[] = []; - - // Copy all logs to sortedLogs first - for (const log of activityLogs) { - sortedLogs[sortedLogs.length] = log; - } - - // Manual bubble sort using for...of loops - for (let i = 0; i < sortedLogs.length; i++) { - for (let j = 0; j < sortedLogs.length - 1 - i; j++) { - const currentLog = sortedLogs[j]; - const nextLog = sortedLogs[j + 1]; - - if ( - currentLog && - nextLog && - new Date(currentLog.createdAt).getTime() > - new Date(nextLog.createdAt).getTime() - ) { - // Swap elements - sortedLogs[j] = nextLog; - sortedLogs[j + 1] = currentLog; - } - } - } - - const logElements: React.ReactElement[] = []; - let index = 0; - - for (const log of sortedLogs) { - const isLast = index === sortedLogs.length - 1; - logElements.push( - , - ); - index++; - } - - return
{logElements}
; - }; - - return ( - - - Activity Log - - {renderActivityLogs()} - - ); + // Sort activity logs and prepare JSX elements using for...of loop + const renderActivityLogs = () => { + if (activityLogs.length === 0) { + return ( +

+ No activity logs available for this transaction +

+ ); + } + + // Sort logs chronologically using for...of loop (manual sorting) + const sortedLogs: ActivityLogEntry[] = []; + + // Copy all logs to sortedLogs first + for (const log of activityLogs) { + sortedLogs[sortedLogs.length] = log; + } + + // Manual bubble sort using for...of loops + for (let i = 0; i < sortedLogs.length; i++) { + for (let j = 0; j < sortedLogs.length - 1 - i; j++) { + const currentLog = sortedLogs[j]; + const nextLog = sortedLogs[j + 1]; + + if ( + currentLog && + nextLog && + new Date(currentLog.createdAt).getTime() > + new Date(nextLog.createdAt).getTime() + ) { + // Swap elements + sortedLogs[j] = nextLog; + sortedLogs[j + 1] = currentLog; + } + } + } + + const logElements: React.ReactElement[] = []; + let index = 0; + + for (const log of sortedLogs) { + const isLast = index === sortedLogs.length - 1; + logElements.push( + , + ); + index++; + } + + return
{logElements}
; + }; + + return ( + + + Activity Log + + {renderActivityLogs()} + + ); } function ActivityLogEntryItem({ - log, - isLast, + log, + isLast, }: { - log: ActivityLogEntry; - isLast: boolean; + log: ActivityLogEntry; + isLast: boolean; }) { - const [isExpanded, setIsExpanded] = useState(false); - - // Get display info based on event type - const getEventTypeInfo = (eventType: string) => { - const type = eventType.toLowerCase(); - if (type.includes("success")) - return { - dot: "bg-green-500", - label: "Success", - variant: "success" as const, - }; - if (type.includes("nack")) - return { - dot: "bg-yellow-500", - label: "Retry", - variant: "warning" as const, - }; - if (type.includes("failure")) - return { - dot: "bg-red-500", - label: "Error", - variant: "destructive" as const, - }; - return { - dot: "bg-primary", - label: eventType, - variant: "secondary" as const, - }; - }; - - const eventInfo = getEventTypeInfo(log.eventType); - - return ( -
- {/* Timeline line */} - {!isLast && ( -
- )} - -
- {/* Timeline dot */} -
-
-
- - {/* Content */} -
- - - {isExpanded && ( -
-
-
-
Executor
-
{log.executorName}
-
-
-
Created At
-
- {format(new Date(log.createdAt), "PP pp z")} -
-
-
- - {log.payload && ( -
-
- Payload -
- -
- )} -
- )} -
-
-
- ); + const [isExpanded, setIsExpanded] = useState(false); + + // Get display info based on event type + const getEventTypeInfo = (eventType: string) => { + const type = eventType.toLowerCase(); + if (type.includes("success")) + return { + dot: "bg-green-500", + label: "Success", + variant: "success" as const, + }; + if (type.includes("nack")) + return { + dot: "bg-yellow-500", + label: "Retry", + variant: "warning" as const, + }; + if (type.includes("failure")) + return { + dot: "bg-red-500", + label: "Error", + variant: "destructive" as const, + }; + return { + dot: "bg-primary", + label: eventType, + variant: "secondary" as const, + }; + }; + + const eventInfo = getEventTypeInfo(log.eventType); + + return ( +
+ {/* Timeline line */} + {!isLast && ( +
+ )} + +
+ {/* Timeline dot */} +
+
+
+ + {/* Content */} +
+ + + {isExpanded && ( +
+
+
+
Executor
+
{log.executorName}
+
+
+
Created At
+
+ {format(new Date(log.createdAt), "PP pp z")} +
+
+
+ + {log.payload && ( +
+
+ Payload +
+ +
+ )} +
+ )} +
+
+
+ ); } From 551256d7866afdc509dc3dc989f2ea7716881516 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 5 Jul 2025 21:24:54 +0000 Subject: [PATCH 05/10] Add switch to raw view when transaction decoding fails Co-authored-by: joaquim.verges --- .../tx/[id]/transaction-details-ui.tsx | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/tx/[id]/transaction-details-ui.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/tx/[id]/transaction-details-ui.tsx index 35f90d9db88..886f003ae7f 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/tx/[id]/transaction-details-ui.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/tx/[id]/transaction-details-ui.tsx @@ -400,7 +400,10 @@ function TransactionParametersCard({ /> {activeTab === "decoded" ? ( - + setActiveTab("raw")} + /> ) : (
{transaction.transactionParams && @@ -424,14 +427,24 @@ function TransactionParametersCard({ // 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. + metadata available.{" "} + + .

); } From 79f10bb897ed477f39ff1fb29ac5f3a9e71b8e8d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 5 Jul 2025 22:02:23 +0000 Subject: [PATCH 06/10] Support multiple transaction parameter decoding in transaction details Co-authored-by: joaquim.verges --- .../(sidebar)/transactions/tx/[id]/page.tsx | 336 +++++++++--------- .../tx/[id]/transaction-details-ui.tsx | 73 +++- 2 files changed, 240 insertions(+), 169 deletions(-) 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 f8d10918a96..a8f1dfe33b8 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,185 +1,203 @@ import { loginRedirect } from "@app/login/loginRedirect"; +import type { AbiFunction } from "abitype"; 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 type { Transaction } from "../../analytics/tx-table/types"; import { - getSingleTransaction, - getTransactionActivityLogs, + 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; - }; + | AbiFunction + | { + type: string; + name?: string; + }; export type DecodedTransactionData = { - contractName: string; - functionName: string; - functionArgs: Record; + contractName: string; + functionName: string; + functionArgs: Record; } | null; -async function decodeTransactionData( - transaction: Transaction, +export type DecodedTransactionResult = DecodedTransactionData[]; + +async function decodeSingleTransactionParam( + txParam: any, + chainId: number, ): Promise { - 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 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 { - contractName, - functionName, - functionArgs, - }; - } catch (error) { - console.error("Error decoding transaction:", error); - return null; - } + try { + if (!txParam || !txParam.to || !txParam.data) { + return null; + } + + // 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 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 { + contractName, + functionName, + functionArgs, + }; + } 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, + params, }: { - params: Promise<{ team_slug: string; project_slug: string; id: string }>; + params: Promise<{ team_slug: string; project_slug: string; id: string }>; }) { - const { team_slug, project_slug, id } = await params; - - const [authToken, project] = await Promise.all([ - getAuthToken(), - getProject(team_slug, project_slug), - ]); - - if (!authToken) { - loginRedirect(`/team/${team_slug}/${project_slug}/transactions/tx/${id}`); - } - - if (!project) { - redirect(`/team/${team_slug}`); - } - - const [transactionData, activityLogs] = await Promise.all([ - getSingleTransaction({ - clientId: project.publishableKey, - teamId: project.teamId, - transactionId: id, - }), - getTransactionActivityLogs({ - clientId: project.publishableKey, - teamId: project.teamId, - transactionId: id, - }), - ]); - - const client = getClientThirdwebClient({ - jwt: authToken, - teamId: project.teamId, - }); - - if (!transactionData) { - notFound(); - } - - // Decode transaction data on the server - const decodedTransactionData = await decodeTransactionData(transactionData); - - return ( -
- -
- ); + const { team_slug, project_slug, id } = await params; + + const [authToken, project] = await Promise.all([ + getAuthToken(), + getProject(team_slug, project_slug), + ]); + + if (!authToken) { + loginRedirect(`/team/${team_slug}/${project_slug}/transactions/tx/${id}`); + } + + if (!project) { + redirect(`/team/${team_slug}`); + } + + const [transactionData, activityLogs] = await Promise.all([ + getSingleTransaction({ + clientId: project.publishableKey, + teamId: project.teamId, + transactionId: id, + }), + getTransactionActivityLogs({ + clientId: project.publishableKey, + teamId: project.teamId, + transactionId: id, + }), + ]); + + const client = getClientThirdwebClient({ + jwt: authToken, + teamId: project.teamId, + }); + + if (!transactionData) { + notFound(); + } + + // Decode transaction data on the server + const decodedTransactionData = await decodeTransactionData(transactionData); + + return ( +
+ +
+ ); } diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/tx/[id]/transaction-details-ui.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/tx/[id]/transaction-details-ui.tsx index 886f003ae7f..28d632a4907 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/tx/[id]/transaction-details-ui.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/tx/[id]/transaction-details-ui.tsx @@ -25,7 +25,7 @@ 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"; +import type { DecodedTransactionData, DecodedTransactionResult } from "./page"; export function TransactionDetailsUI({ transaction, @@ -38,7 +38,7 @@ export function TransactionDetailsUI({ client: ThirdwebClient; project: Project; activityLogs: ActivityLogEntry[]; - decodedTransactionData: DecodedTransactionData; + decodedTransactionData: DecodedTransactionResult; }) { const { idToChain } = useAllChainsData(); @@ -373,7 +373,7 @@ function TransactionParametersCard({ decodedTransactionData, }: { transaction: Transaction; - decodedTransactionData: DecodedTransactionData; + decodedTransactionData: DecodedTransactionResult; }) { const [activeTab, setActiveTab] = useState<"decoded" | "raw">("decoded"); @@ -400,8 +400,8 @@ function TransactionParametersCard({ /> {activeTab === "decoded" ? ( - setActiveTab("raw")} /> ) : ( @@ -424,15 +424,15 @@ function TransactionParametersCard({ ); } -// Client component to display decoded transaction data -function DecodedTransactionDisplay({ - decodedData, +// Client component to display list of decoded transaction data +function DecodedTransactionListDisplay({ + decodedDataList, onSwitchToRaw, }: { - decodedData: DecodedTransactionData; + decodedDataList: DecodedTransactionResult; onSwitchToRaw: () => void; }) { - if (!decodedData) { + if (decodedDataList.length === 0) { return (

Unable to decode transaction data. The contract may not have verified @@ -449,8 +449,61 @@ function DecodedTransactionDisplay({ ); } + return ( +

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

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

+
+ ); + } + return (
+
+ Transaction {transactionIndex + 1} +
Contract Name
{decodedData.contractName}
From 2bf4b91e3f029a3a4dfa0be6ac88a1fac7a00c9c Mon Sep 17 00:00:00 2001 From: Joaquim Verges Date: Sun, 6 Jul 2025 13:23:11 +1200 Subject: [PATCH 07/10] cleanup --- .../(sidebar)/transactions/tx/[id]/page.tsx | 340 ++--- .../tx/[id]/transaction-details-ui.tsx | 1276 ++++++++--------- 2 files changed, 809 insertions(+), 807 deletions(-) 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 a8f1dfe33b8..588d3533b0b 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 @@ -11,193 +11,197 @@ 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, + getSingleTransaction, + getTransactionActivityLogs, } from "../../lib/analytics"; import { TransactionDetailsUI } from "./transaction-details-ui"; type AbiItem = - | AbiFunction - | { - type: string; - name?: string; - }; + | AbiFunction + | { + type: string; + name?: string; + }; export type DecodedTransactionData = { - contractName: string; - functionName: string; - functionArgs: Record; + contractName: string; + functionName: string; + functionArgs: Record; } | null; export type DecodedTransactionResult = DecodedTransactionData[]; async function decodeSingleTransactionParam( - txParam: any, - chainId: number, + txParam: { + to: string; + data: `0x${string}`; + }, + chainId: number, ): Promise { - try { - if (!txParam || !txParam.to || !txParam.data) { - return null; - } - - // 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 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 { - contractName, - functionName, - functionArgs, - }; - } catch (error) { - console.error("Error decoding transaction param:", error); - return null; - } + try { + if (!txParam || !txParam.to || !txParam.data) { + return null; + } + + // Create contract instance + const contract = getContract({ + address: txParam.to, + // eslint-disable-next-line no-restricted-syntax + chain: defineChain(chainId), + client: serverThirdwebClient, + }); + + // 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 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 { + contractName, + functionArgs, + functionName, + }; + } catch (error) { + console.error("Error decoding transaction param:", error); + return null; + } } async function decodeTransactionData( - transaction: Transaction, + 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 []; - } + 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, + params, }: { - params: Promise<{ team_slug: string; project_slug: string; id: string }>; + params: Promise<{ team_slug: string; project_slug: string; id: string }>; }) { - const { team_slug, project_slug, id } = await params; - - const [authToken, project] = await Promise.all([ - getAuthToken(), - getProject(team_slug, project_slug), - ]); - - if (!authToken) { - loginRedirect(`/team/${team_slug}/${project_slug}/transactions/tx/${id}`); - } - - if (!project) { - redirect(`/team/${team_slug}`); - } - - const [transactionData, activityLogs] = await Promise.all([ - getSingleTransaction({ - clientId: project.publishableKey, - teamId: project.teamId, - transactionId: id, - }), - getTransactionActivityLogs({ - clientId: project.publishableKey, - teamId: project.teamId, - transactionId: id, - }), - ]); - - const client = getClientThirdwebClient({ - jwt: authToken, - teamId: project.teamId, - }); - - if (!transactionData) { - notFound(); - } - - // Decode transaction data on the server - const decodedTransactionData = await decodeTransactionData(transactionData); - - return ( -
- -
- ); + const { team_slug, project_slug, id } = await params; + + const [authToken, project] = await Promise.all([ + getAuthToken(), + getProject(team_slug, project_slug), + ]); + + if (!authToken) { + loginRedirect(`/team/${team_slug}/${project_slug}/transactions/tx/${id}`); + } + + if (!project) { + redirect(`/team/${team_slug}`); + } + + const [transactionData, activityLogs] = await Promise.all([ + getSingleTransaction({ + clientId: project.publishableKey, + teamId: project.teamId, + transactionId: id, + }), + getTransactionActivityLogs({ + clientId: project.publishableKey, + teamId: project.teamId, + transactionId: id, + }), + ]); + + const client = getClientThirdwebClient({ + jwt: authToken, + teamId: project.teamId, + }); + + if (!transactionData) { + notFound(); + } + + // Decode transaction data on the server + const decodedTransactionData = await decodeTransactionData(transactionData); + + return ( +
+ +
+ ); } diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/tx/[id]/transaction-details-ui.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/tx/[id]/transaction-details-ui.tsx index 28d632a4907..920d4e9fe21 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/tx/[id]/transaction-details-ui.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/tx/[id]/transaction-details-ui.tsx @@ -2,10 +2,10 @@ import { format, formatDistanceToNowStrict } from "date-fns"; import { - ChevronDownIcon, - ChevronRightIcon, - ExternalLinkIcon, - InfoIcon, + ChevronDownIcon, + ChevronRightIcon, + ExternalLinkIcon, + InfoIcon, } from "lucide-react"; import Link from "next/link"; import { useState } from "react"; @@ -28,670 +28,668 @@ import type { ActivityLogEntry } from "../../lib/analytics"; import type { DecodedTransactionData, DecodedTransactionResult } from "./page"; export function TransactionDetailsUI({ - transaction, - client, - activityLogs, - decodedTransactionData, + transaction, + client, + activityLogs, + decodedTransactionData, }: { - transaction: Transaction; - teamSlug: string; - client: ThirdwebClient; - project: Project; - activityLogs: ActivityLogEntry[]; - decodedTransactionData: DecodedTransactionResult; + transaction: Transaction; + teamSlug: string; + client: ThirdwebClient; + project: Project; + activityLogs: ActivityLogEntry[]; + decodedTransactionData: DecodedTransactionResult; }) { - const { idToChain } = useAllChainsData(); - - // Extract relevant data from transaction - const { - id, - chainId, - from, - transactionHash, - confirmedAt, - createdAt, - executionParams, - executionResult, - } = transaction; - - const status = executionResult?.status as keyof typeof statusDetails; - const errorMessage = - executionResult && "error" in executionResult - ? executionResult.error.message - : executionResult && "revertData" in executionResult - ? executionResult.revertData?.revertReason - : null; - const errorDetails = - executionResult && "error" in executionResult - ? executionResult.error - : undefined; - - const chain = chainId ? idToChain.get(Number.parseInt(chainId)) : undefined; - const explorer = chain?.explorers?.[0]; - - // Calculate time difference between creation and confirmation - const confirmationTime = - confirmedAt && createdAt - ? new Date(confirmedAt).getTime() - new Date(createdAt).getTime() - : null; - - // Determine sender and signer addresses - const senderAddress = executionParams?.smartAccountAddress || from || ""; - const signerAddress = executionParams?.signerAddress || from || ""; - - // Gas information - const gasUsed = - executionResult && "actualGasUsed" in executionResult - ? `${ - isHex(executionResult.actualGasUsed) - ? hexToNumber(executionResult.actualGasUsed) - : executionResult.actualGasUsed - }` - : "N/A"; - - const gasCost = - executionResult && "actualGasCost" in executionResult - ? `${toEther(BigInt(executionResult.actualGasCost || "0"))} ${ - chain?.nativeCurrency.symbol || "" - }` - : "N/A"; - - return ( - <> -
-

- Transaction Details -

-
- -
- {/* Transaction Info Card */} - - - Transaction Information - - -
-
- Status -
-
- {status && ( - - - {statusDetails[status].name} - {errorMessage && } - - - )} -
-
- -
-
- Transaction ID -
-
- -
-
- -
-
- Transaction Hash -
-
- {transactionHash ? ( -
- {explorer ? ( - - ) : ( - - )} -
- ) : ( -
Not available yet
- )} -
-
- -
-
- Network -
-
- {chain ? ( -
- - {chain.name || "Unknown"} -
- ) : ( -
Chain ID: {chainId || "Unknown"}
- )} -
-
-
-
- - - Sender Information - - -
-
- Sender Address -
-
- -
-
- -
-
- Signer Address -
-
- -
-
-
-
- - {errorMessage && ( - - - - Error Details - - - - {errorDetails ? ( - - ) : ( -
- {errorMessage} -
- )} -
-
- )} - - - Timing Information - - -
-
- Created At -
-
- {createdAt ? ( -

- {formatDistanceToNowStrict(new Date(createdAt), { - addSuffix: true, - })}{" "} - ({format(new Date(createdAt), "PP pp z")}) -

- ) : ( - "N/A" - )} -
-
- -
-
- Confirmed At -
-
- {confirmedAt ? ( -

- {formatDistanceToNowStrict(new Date(confirmedAt), { - addSuffix: true, - })}{" "} - ({format(new Date(confirmedAt), "PP pp z")}) -

- ) : ( - "Pending" - )} -
-
- - {confirmationTime && ( -
-
- Confirmation Time -
-
- {Math.floor(confirmationTime / 1000)} seconds -
-
- )} -
-
- - - Gas Information - - -
-
- Gas Used -
-
{gasUsed}
-
- -
-
- Gas Cost -
-
{gasCost}
-
- - {transaction.confirmedAtBlockNumber && ( -
-
- Block Number -
-
- {isHex(transaction.confirmedAtBlockNumber) - ? hexToNumber(transaction.confirmedAtBlockNumber) - : transaction.confirmedAtBlockNumber} -
-
- )} -
-
- - {/* Activity Log Card */} - -
- - ); + const { idToChain } = useAllChainsData(); + + // Extract relevant data from transaction + const { + id, + chainId, + from, + transactionHash, + confirmedAt, + createdAt, + executionParams, + executionResult, + } = transaction; + + const status = executionResult?.status as keyof typeof statusDetails; + const errorMessage = + executionResult && "error" in executionResult + ? executionResult.error.message + : executionResult && "revertData" in executionResult + ? executionResult.revertData?.revertReason + : null; + const errorDetails = + executionResult && "error" in executionResult + ? executionResult.error + : undefined; + + const chain = chainId ? idToChain.get(Number.parseInt(chainId)) : undefined; + const explorer = chain?.explorers?.[0]; + + // Calculate time difference between creation and confirmation + const confirmationTime = + confirmedAt && createdAt + ? new Date(confirmedAt).getTime() - new Date(createdAt).getTime() + : null; + + // Determine sender and signer addresses + const senderAddress = executionParams?.smartAccountAddress || from || ""; + const signerAddress = executionParams?.signerAddress || from || ""; + + // Gas information + const gasUsed = + executionResult && "actualGasUsed" in executionResult + ? `${ + isHex(executionResult.actualGasUsed) + ? hexToNumber(executionResult.actualGasUsed) + : executionResult.actualGasUsed + }` + : "N/A"; + + const gasCost = + executionResult && "actualGasCost" in executionResult + ? `${toEther(BigInt(executionResult.actualGasCost || "0"))} ${ + chain?.nativeCurrency.symbol || "" + }` + : "N/A"; + + return ( + <> +
+

+ Transaction Details +

+
+ +
+ {/* Transaction Info Card */} + + + Transaction Information + + +
+
+ Status +
+
+ {status && ( + + + {statusDetails[status].name} + {errorMessage && } + + + )} +
+
+ +
+
+ Transaction ID +
+
+ +
+
+ +
+
+ Transaction Hash +
+
+ {transactionHash ? ( +
+ {explorer ? ( + + ) : ( + + )} +
+ ) : ( +
Not available yet
+ )} +
+
+ +
+
+ Network +
+
+ {chain ? ( +
+ + {chain.name || "Unknown"} +
+ ) : ( +
Chain ID: {chainId || "Unknown"}
+ )} +
+
+
+
+ + + Sender Information + + +
+
+ Sender Address +
+
+ +
+
+ +
+
+ Signer Address +
+
+ +
+
+
+
+ + {errorMessage && ( + + + + Error Details + + + + {errorDetails ? ( + + ) : ( +
+ {errorMessage} +
+ )} +
+
+ )} + + + Timing Information + + +
+
+ Created At +
+
+ {createdAt ? ( +

+ {formatDistanceToNowStrict(new Date(createdAt), { + addSuffix: true, + })}{" "} + ({format(new Date(createdAt), "PP pp z")}) +

+ ) : ( + "N/A" + )} +
+
+ +
+
+ Confirmed At +
+
+ {confirmedAt ? ( +

+ {formatDistanceToNowStrict(new Date(confirmedAt), { + addSuffix: true, + })}{" "} + ({format(new Date(confirmedAt), "PP pp z")}) +

+ ) : ( + "Pending" + )} +
+
+ + {confirmationTime && ( +
+
+ Confirmation Time +
+
+ {Math.floor(confirmationTime / 1000)} seconds +
+
+ )} +
+
+ + + Gas Information + + +
+
+ Gas Used +
+
{gasUsed}
+
+ +
+
+ Gas Cost +
+
{gasCost}
+
+ + {transaction.confirmedAtBlockNumber && ( +
+
+ Block Number +
+
+ {isHex(transaction.confirmedAtBlockNumber) + ? hexToNumber(transaction.confirmedAtBlockNumber) + : transaction.confirmedAtBlockNumber} +
+
+ )} +
+
+ + {/* Activity Log Card */} + +
+ + ); } // Transaction Parameters Card with Tabs function TransactionParametersCard({ - transaction, - decodedTransactionData, + transaction, + decodedTransactionData, }: { - transaction: Transaction; - decodedTransactionData: DecodedTransactionResult; + 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 -

- )} -
- )} -
-
- ); + 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, + onSwitchToRaw, }: { - decodedDataList: DecodedTransactionResult; - onSwitchToRaw: () => void; + 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) => ( -
- {index > 0 &&
} - -
- ), - )} -
- ); + 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) => { + if (!decodedData) { + return null; + } + return ( +
+ {index > 0 &&
} + +
+ ); + }, + )} +
+ ); } // Client component to display decoded transaction data function DecodedTransactionDisplay({ - decodedData, - onSwitchToRaw, - transactionIndex, + decodedData, + onSwitchToRaw, }: { - decodedData: DecodedTransactionData; - onSwitchToRaw: () => void; - transactionIndex: number; + decodedData: DecodedTransactionData; + onSwitchToRaw: () => void; }) { - if (!decodedData) { - return ( -
-
- Transaction {transactionIndex + 1} -
-

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

-
- ); - } - - return ( -
-
- Transaction {transactionIndex + 1} -
-
-
Contract Name
-
{decodedData.contractName}
-
-
-
Function Name
-
{decodedData.functionName}
-
-
-
Function Arguments
- -
-
- ); + if (!decodedData) { + return ( +
+

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

+
+ ); + } + + return ( +
+
+
Contract Name
+
{decodedData.contractName}
+
+
+
Function Name
+
{decodedData.functionName}
+
+
+
Function Arguments
+ +
+
+ ); } // Activity Log Timeline Component function ActivityLogCard({ - activityLogs, + activityLogs, }: { - activityLogs: ActivityLogEntry[]; + activityLogs: ActivityLogEntry[]; }) { - // Sort activity logs and prepare JSX elements using for...of loop - const renderActivityLogs = () => { - if (activityLogs.length === 0) { - return ( -

- No activity logs available for this transaction -

- ); - } - - // Sort logs chronologically using for...of loop (manual sorting) - const sortedLogs: ActivityLogEntry[] = []; - - // Copy all logs to sortedLogs first - for (const log of activityLogs) { - sortedLogs[sortedLogs.length] = log; - } - - // Manual bubble sort using for...of loops - for (let i = 0; i < sortedLogs.length; i++) { - for (let j = 0; j < sortedLogs.length - 1 - i; j++) { - const currentLog = sortedLogs[j]; - const nextLog = sortedLogs[j + 1]; - - if ( - currentLog && - nextLog && - new Date(currentLog.createdAt).getTime() > - new Date(nextLog.createdAt).getTime() - ) { - // Swap elements - sortedLogs[j] = nextLog; - sortedLogs[j + 1] = currentLog; - } - } - } - - const logElements: React.ReactElement[] = []; - let index = 0; - - for (const log of sortedLogs) { - const isLast = index === sortedLogs.length - 1; - logElements.push( - , - ); - index++; - } - - return
{logElements}
; - }; - - return ( - - - Activity Log - - {renderActivityLogs()} - - ); + // Sort activity logs and prepare JSX elements using for...of loop + const renderActivityLogs = () => { + if (activityLogs.length === 0) { + return ( +

+ No activity logs available for this transaction +

+ ); + } + + // Sort logs chronologically using for...of loop (manual sorting) + const sortedLogs: ActivityLogEntry[] = []; + + // Copy all logs to sortedLogs first + for (const log of activityLogs) { + sortedLogs[sortedLogs.length] = log; + } + + // Manual bubble sort using for...of loops + for (let i = 0; i < sortedLogs.length; i++) { + for (let j = 0; j < sortedLogs.length - 1 - i; j++) { + const currentLog = sortedLogs[j]; + const nextLog = sortedLogs[j + 1]; + + if ( + currentLog && + nextLog && + new Date(currentLog.createdAt).getTime() > + new Date(nextLog.createdAt).getTime() + ) { + // Swap elements + sortedLogs[j] = nextLog; + sortedLogs[j + 1] = currentLog; + } + } + } + + const logElements: React.ReactElement[] = []; + let index = 0; + + for (const log of sortedLogs) { + const isLast = index === sortedLogs.length - 1; + logElements.push( + , + ); + index++; + } + + return
{logElements}
; + }; + + return ( + + + Activity Log + + {renderActivityLogs()} + + ); } function ActivityLogEntryItem({ - log, - isLast, + log, + isLast, }: { - log: ActivityLogEntry; - isLast: boolean; + log: ActivityLogEntry; + isLast: boolean; }) { - const [isExpanded, setIsExpanded] = useState(false); - - // Get display info based on event type - const getEventTypeInfo = (eventType: string) => { - const type = eventType.toLowerCase(); - if (type.includes("success")) - return { - dot: "bg-green-500", - label: "Success", - variant: "success" as const, - }; - if (type.includes("nack")) - return { - dot: "bg-yellow-500", - label: "Retry", - variant: "warning" as const, - }; - if (type.includes("failure")) - return { - dot: "bg-red-500", - label: "Error", - variant: "destructive" as const, - }; - return { - dot: "bg-primary", - label: eventType, - variant: "secondary" as const, - }; - }; - - const eventInfo = getEventTypeInfo(log.eventType); - - return ( -
- {/* Timeline line */} - {!isLast && ( -
- )} - -
- {/* Timeline dot */} -
-
-
- - {/* Content */} -
- - - {isExpanded && ( -
-
-
-
Executor
-
{log.executorName}
-
-
-
Created At
-
- {format(new Date(log.createdAt), "PP pp z")} -
-
-
- - {log.payload && ( -
-
- Payload -
- -
- )} -
- )} -
-
-
- ); + const [isExpanded, setIsExpanded] = useState(false); + + // Get display info based on event type + const getEventTypeInfo = (eventType: string) => { + const type = eventType.toLowerCase(); + if (type.includes("success")) + return { + dot: "bg-green-500", + label: "Success", + variant: "success" as const, + }; + if (type.includes("nack")) + return { + dot: "bg-yellow-500", + label: "Retry", + variant: "warning" as const, + }; + if (type.includes("failure")) + return { + dot: "bg-red-500", + label: "Error", + variant: "destructive" as const, + }; + return { + dot: "bg-primary", + label: eventType, + variant: "secondary" as const, + }; + }; + + const eventInfo = getEventTypeInfo(log.eventType); + + return ( +
+ {/* Timeline line */} + {!isLast && ( +
+ )} + +
+ {/* Timeline dot */} +
+
+
+ + {/* Content */} +
+ + + {isExpanded && ( +
+
+
+
Executor
+
{log.executorName}
+
+
+
Created At
+
+ {format(new Date(log.createdAt), "PP pp z")} +
+
+
+ + {log.payload && ( +
+
+ Payload +
+ +
+ )} +
+ )} +
+
+
+ ); } From b964fc1839f44a7d7f055a4d4aa4f582398ddbd7 Mon Sep 17 00:00:00 2001 From: Joaquim Verges Date: Sun, 6 Jul 2025 16:01:54 +1200 Subject: [PATCH 08/10] layout --- .../tx/[id]/transaction-details-ui.tsx | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/tx/[id]/transaction-details-ui.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/tx/[id]/transaction-details-ui.tsx index 920d4e9fe21..97b814d5d22 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/tx/[id]/transaction-details-ui.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/tx/[id]/transaction-details-ui.tsx @@ -502,16 +502,18 @@ function DecodedTransactionDisplay({ return (
-
-
Contract Name
-
{decodedData.contractName}
-
-
-
Function Name
-
{decodedData.functionName}
+
+
+
Function
+
{decodedData.functionName}
+
+
+
Contract
+
{decodedData.contractName}
+
-
Function Arguments
+
Arguments
Date: Sun, 6 Jul 2025 19:03:55 +1200 Subject: [PATCH 09/10] clickable contract --- .../(sidebar)/transactions/tx/[id]/page.tsx | 4 ++++ .../transactions/tx/[id]/transaction-details-ui.tsx | 8 ++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) 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 588d3533b0b..28c5018dae2 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 @@ -24,6 +24,8 @@ type AbiItem = }; export type DecodedTransactionData = { + chainId: number; + contractAddress: string; contractName: string; functionName: string; functionArgs: Record; @@ -105,6 +107,8 @@ async function decodeSingleTransactionParam( } return { + chainId, + contractAddress: txParam.to, contractName, functionArgs, functionName, diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/tx/[id]/transaction-details-ui.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/tx/[id]/transaction-details-ui.tsx index 97b814d5d22..f0b465d23c3 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/tx/[id]/transaction-details-ui.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/tx/[id]/transaction-details-ui.tsx @@ -10,7 +10,7 @@ import { import Link from "next/link"; import { useState } from "react"; import { hexToNumber, isHex, type ThirdwebClient, toEther } from "thirdweb"; -import { stringify } from "thirdweb/utils"; +import { stringify,shortenAddress } from "thirdweb/utils"; import type { Project } from "@/api/projects"; import { WalletAddress } from "@/components/blocks/wallet-address"; import { Badge } from "@/components/ui/badge"; @@ -509,7 +509,11 @@ function DecodedTransactionDisplay({
Contract
-
{decodedData.contractName}
+
+ + {decodedData.contractName} + +
From 551a4eafc195c509db3951b21368949b78cb9a43 Mon Sep 17 00:00:00 2001 From: Joaquim Verges Date: Sun, 6 Jul 2025 19:26:16 +1200 Subject: [PATCH 10/10] cleanup --- .../(sidebar)/transactions/tx/[id]/page.tsx | 34 ++++++++++++++++--- .../tx/[id]/transaction-details-ui.tsx | 29 +++++++++------- 2 files changed, 46 insertions(+), 17 deletions(-) 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 28c5018dae2..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,10 +1,14 @@ import { loginRedirect } from "@app/login/loginRedirect"; import type { AbiFunction } from "abitype"; import { notFound, redirect } from "next/navigation"; -import { getContract } from "thirdweb"; -import { defineChain } from "thirdweb/chains"; +import { getContract, toTokens } from "thirdweb"; +import { defineChain, getChainMetadata } from "thirdweb/chains"; import { getCompilerMetadata } from "thirdweb/contract"; -import { decodeFunctionData, toFunctionSelector } from "thirdweb/utils"; +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"; @@ -26,6 +30,7 @@ type AbiItem = export type DecodedTransactionData = { chainId: number; contractAddress: string; + value: string; contractName: string; functionName: string; functionArgs: Record; @@ -37,6 +42,7 @@ async function decodeSingleTransactionParam( txParam: { to: string; data: `0x${string}`; + value: string; }, chainId: number, ): Promise { @@ -45,15 +51,32 @@ async function decodeSingleTransactionParam( return null; } + // eslint-disable-next-line no-restricted-syntax + const chain = defineChain(chainId); + // Create contract instance const contract = getContract({ address: txParam.to, - // eslint-disable-next-line no-restricted-syntax - chain: defineChain(chainId), + 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) { @@ -112,6 +135,7 @@ async function decodeSingleTransactionParam( contractName, functionArgs, functionName, + value: txValue, }; } catch (error) { console.error("Error decoding transaction param:", error); diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/tx/[id]/transaction-details-ui.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/tx/[id]/transaction-details-ui.tsx index f0b465d23c3..0851bef4466 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/tx/[id]/transaction-details-ui.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/tx/[id]/transaction-details-ui.tsx @@ -10,7 +10,7 @@ import { import Link from "next/link"; import { useState } from "react"; import { hexToNumber, isHex, type ThirdwebClient, toEther } from "thirdweb"; -import { stringify,shortenAddress } from "thirdweb/utils"; +import { stringify } from "thirdweb/utils"; import type { Project } from "@/api/projects"; import { WalletAddress } from "@/components/blocks/wallet-address"; import { Badge } from "@/components/ui/badge"; @@ -453,12 +453,9 @@ function DecodedTransactionListDisplay({
{decodedDataList.map( (decodedData: DecodedTransactionData, index: number) => { - if (!decodedData) { - return null; - } return (
{index > 0 &&
}
-
Function
-
{decodedData.functionName}
-
-
-
Contract
+
Target
- + {decodedData.contractName}
+
+
Function
+
{decodedData.functionName}
+
+
+
Value
+
{decodedData.value}
+
-
+
Arguments