Skip to content

Commit 770e7b2

Browse files
Enhance transaction details with decoded tab (#7531)
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
1 parent ff9df49 commit 770e7b2

File tree

2 files changed

+345
-19
lines changed

2 files changed

+345
-19
lines changed

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/tx/[id]/page.tsx

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,180 @@
11
import { loginRedirect } from "@app/login/loginRedirect";
2+
import type { AbiFunction } from "abitype";
23
import { notFound, redirect } from "next/navigation";
4+
import { getContract, toTokens } from "thirdweb";
5+
import { defineChain, getChainMetadata } from "thirdweb/chains";
6+
import { getCompilerMetadata } from "thirdweb/contract";
7+
import {
8+
decodeFunctionData,
9+
shortenAddress,
10+
toFunctionSelector,
11+
} from "thirdweb/utils";
312
import { getAuthToken } from "@/api/auth-token";
413
import { getProject } from "@/api/projects";
514
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
15+
import { serverThirdwebClient } from "@/constants/thirdweb-client.server";
16+
import type { Transaction } from "../../analytics/tx-table/types";
617
import {
718
getSingleTransaction,
819
getTransactionActivityLogs,
920
} from "../../lib/analytics";
1021
import { TransactionDetailsUI } from "./transaction-details-ui";
1122

23+
type AbiItem =
24+
| AbiFunction
25+
| {
26+
type: string;
27+
name?: string;
28+
};
29+
30+
export type DecodedTransactionData = {
31+
chainId: number;
32+
contractAddress: string;
33+
value: string;
34+
contractName: string;
35+
functionName: string;
36+
functionArgs: Record<string, unknown>;
37+
} | null;
38+
39+
export type DecodedTransactionResult = DecodedTransactionData[];
40+
41+
async function decodeSingleTransactionParam(
42+
txParam: {
43+
to: string;
44+
data: `0x${string}`;
45+
value: string;
46+
},
47+
chainId: number,
48+
): Promise<DecodedTransactionData> {
49+
try {
50+
if (!txParam || !txParam.to || !txParam.data) {
51+
return null;
52+
}
53+
54+
// eslint-disable-next-line no-restricted-syntax
55+
const chain = defineChain(chainId);
56+
57+
// Create contract instance
58+
const contract = getContract({
59+
address: txParam.to,
60+
chain,
61+
client: serverThirdwebClient,
62+
});
63+
64+
// Fetch compiler metadata
65+
const chainMetadata = await getChainMetadata(chain);
66+
67+
const txValue = `${txParam.value ? toTokens(BigInt(txParam.value), chainMetadata.nativeCurrency.decimals) : "0"} ${chainMetadata.nativeCurrency.symbol}`;
68+
69+
if (txParam.data === "0x") {
70+
return {
71+
chainId,
72+
contractAddress: txParam.to,
73+
contractName: shortenAddress(txParam.to),
74+
functionArgs: {},
75+
functionName: "Transfer",
76+
value: txValue,
77+
};
78+
}
79+
80+
const compilerMetadata = await getCompilerMetadata(contract);
81+
82+
if (!compilerMetadata || !compilerMetadata.abi) {
83+
return null;
84+
}
85+
86+
const contractName = compilerMetadata.name || "Unknown Contract";
87+
const abi = compilerMetadata.abi;
88+
89+
// Extract function selector from transaction data (first 4 bytes)
90+
const functionSelector = txParam.data.slice(0, 10) as `0x${string}`;
91+
92+
// Find matching function in ABI
93+
const functions = (abi as readonly AbiItem[]).filter(
94+
(item): item is AbiFunction => item.type === "function",
95+
);
96+
let matchingFunction: AbiFunction | null = null;
97+
98+
for (const func of functions) {
99+
const selector = toFunctionSelector(func);
100+
if (selector === functionSelector) {
101+
matchingFunction = func;
102+
break;
103+
}
104+
}
105+
106+
if (!matchingFunction) {
107+
return null;
108+
}
109+
110+
const functionName = matchingFunction.name;
111+
112+
// Decode function data
113+
const decodedArgs = (await decodeFunctionData({
114+
contract: getContract({
115+
...contract,
116+
abi: [matchingFunction],
117+
}),
118+
data: txParam.data,
119+
})) as readonly unknown[];
120+
121+
// Create a clean object for display
122+
const functionArgs: Record<string, unknown> = {};
123+
if (matchingFunction.inputs && decodedArgs) {
124+
for (let index = 0; index < matchingFunction.inputs.length; index++) {
125+
const input = matchingFunction.inputs[index];
126+
if (input) {
127+
functionArgs[input.name || `arg${index}`] = decodedArgs[index];
128+
}
129+
}
130+
}
131+
132+
return {
133+
chainId,
134+
contractAddress: txParam.to,
135+
contractName,
136+
functionArgs,
137+
functionName,
138+
value: txValue,
139+
};
140+
} catch (error) {
141+
console.error("Error decoding transaction param:", error);
142+
return null;
143+
}
144+
}
145+
146+
async function decodeTransactionData(
147+
transaction: Transaction,
148+
): Promise<DecodedTransactionResult> {
149+
try {
150+
// Check if we have transaction parameters
151+
if (
152+
!transaction.transactionParams ||
153+
transaction.transactionParams.length === 0
154+
) {
155+
return [];
156+
}
157+
158+
// Ensure we have a chainId
159+
if (!transaction.chainId) {
160+
return [];
161+
}
162+
163+
const chainId = parseInt(transaction.chainId);
164+
165+
// Decode all transaction parameters in parallel
166+
const decodingPromises = transaction.transactionParams.map((txParam) =>
167+
decodeSingleTransactionParam(txParam, chainId),
168+
);
169+
170+
const results = await Promise.all(decodingPromises);
171+
return results;
172+
} catch (error) {
173+
console.error("Error decoding transaction:", error);
174+
return [];
175+
}
176+
}
177+
12178
export default async function TransactionPage({
13179
params,
14180
}: {
@@ -51,11 +217,15 @@ export default async function TransactionPage({
51217
notFound();
52218
}
53219

220+
// Decode transaction data on the server
221+
const decodedTransactionData = await decodeTransactionData(transactionData);
222+
54223
return (
55224
<div className="space-y-6 p-2">
56225
<TransactionDetailsUI
57226
activityLogs={activityLogs}
58227
client={client}
228+
decodedTransactionData={decodedTransactionData}
59229
project={project}
60230
teamSlug={team_slug}
61231
transaction={transactionData}

0 commit comments

Comments
 (0)