Skip to content

Commit 79f10bb

Browse files
Support multiple transaction parameter decoding in transaction details
Co-authored-by: joaquim.verges <[email protected]>
1 parent 551256d commit 79f10bb

File tree

2 files changed

+240
-169
lines changed

2 files changed

+240
-169
lines changed
Lines changed: 177 additions & 159 deletions
Original file line numberDiff line numberDiff line change
@@ -1,185 +1,203 @@
11
import { loginRedirect } from "@app/login/loginRedirect";
2+
import type { AbiFunction } from "abitype";
23
import { notFound, redirect } from "next/navigation";
34
import { getContract } from "thirdweb";
45
import { defineChain } from "thirdweb/chains";
56
import { getCompilerMetadata } from "thirdweb/contract";
67
import { decodeFunctionData, toFunctionSelector } from "thirdweb/utils";
7-
import type { AbiFunction } from "abitype";
88
import { getAuthToken } from "@/api/auth-token";
99
import { getProject } from "@/api/projects";
1010
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
1111
import { serverThirdwebClient } from "@/constants/thirdweb-client.server";
12+
import type { Transaction } from "../../analytics/tx-table/types";
1213
import {
13-
getSingleTransaction,
14-
getTransactionActivityLogs,
14+
getSingleTransaction,
15+
getTransactionActivityLogs,
1516
} from "../../lib/analytics";
16-
import type { Transaction } from "../../analytics/tx-table/types";
1717
import { TransactionDetailsUI } from "./transaction-details-ui";
1818

1919
type AbiItem =
20-
| AbiFunction
21-
| {
22-
type: string;
23-
name?: string;
24-
};
20+
| AbiFunction
21+
| {
22+
type: string;
23+
name?: string;
24+
};
2525

2626
export type DecodedTransactionData = {
27-
contractName: string;
28-
functionName: string;
29-
functionArgs: Record<string, unknown>;
27+
contractName: string;
28+
functionName: string;
29+
functionArgs: Record<string, unknown>;
3030
} | null;
3131

32-
async function decodeTransactionData(
33-
transaction: Transaction,
32+
export type DecodedTransactionResult = DecodedTransactionData[];
33+
34+
async function decodeSingleTransactionParam(
35+
txParam: any,
36+
chainId: number,
3437
): Promise<DecodedTransactionData> {
35-
try {
36-
// Check if we have transaction parameters
37-
if (
38-
!transaction.transactionParams ||
39-
transaction.transactionParams.length === 0
40-
) {
41-
return null;
42-
}
43-
44-
// Get the first transaction parameter (assuming single transaction)
45-
const txParam = transaction.transactionParams[0];
46-
if (!txParam || !txParam.to || !txParam.data) {
47-
return null;
48-
}
49-
50-
// Ensure we have a chainId
51-
if (!transaction.chainId) {
52-
return null;
53-
}
54-
55-
const chainId = parseInt(transaction.chainId);
56-
57-
// Create contract instance
58-
const contract = getContract({
59-
client: serverThirdwebClient,
60-
address: txParam.to,
61-
chain: defineChain(chainId),
62-
});
63-
64-
// Fetch compiler metadata
65-
const compilerMetadata = await getCompilerMetadata(contract);
66-
67-
if (!compilerMetadata || !compilerMetadata.abi) {
68-
return null;
69-
}
70-
71-
const contractName = compilerMetadata.name || "Unknown Contract";
72-
const abi = compilerMetadata.abi;
73-
74-
// Extract function selector from transaction data (first 4 bytes)
75-
const functionSelector = txParam.data.slice(0, 10) as `0x${string}`;
76-
77-
// Find matching function in ABI
78-
const functions = (abi as readonly AbiItem[]).filter(
79-
(item): item is AbiFunction => item.type === "function",
80-
);
81-
let matchingFunction: AbiFunction | null = null;
82-
83-
for (const func of functions) {
84-
const selector = toFunctionSelector(func);
85-
if (selector === functionSelector) {
86-
matchingFunction = func;
87-
break;
88-
}
89-
}
90-
91-
if (!matchingFunction) {
92-
return null;
93-
}
94-
95-
const functionName = matchingFunction.name;
96-
97-
// Decode function data
98-
const decodedArgs = (await decodeFunctionData({
99-
contract: getContract({
100-
...contract,
101-
abi: [matchingFunction],
102-
}),
103-
data: txParam.data,
104-
})) as readonly unknown[];
105-
106-
// Create a clean object for display
107-
const functionArgs: Record<string, unknown> = {};
108-
if (matchingFunction.inputs && decodedArgs) {
109-
for (let index = 0; index < matchingFunction.inputs.length; index++) {
110-
const input = matchingFunction.inputs[index];
111-
if (input) {
112-
functionArgs[input.name || `arg${index}`] = decodedArgs[index];
113-
}
114-
}
115-
}
116-
117-
return {
118-
contractName,
119-
functionName,
120-
functionArgs,
121-
};
122-
} catch (error) {
123-
console.error("Error decoding transaction:", error);
124-
return null;
125-
}
38+
try {
39+
if (!txParam || !txParam.to || !txParam.data) {
40+
return null;
41+
}
42+
43+
// Create contract instance
44+
const contract = getContract({
45+
client: serverThirdwebClient,
46+
address: txParam.to,
47+
chain: defineChain(chainId),
48+
});
49+
50+
// Fetch compiler metadata
51+
const compilerMetadata = await getCompilerMetadata(contract);
52+
53+
if (!compilerMetadata || !compilerMetadata.abi) {
54+
return null;
55+
}
56+
57+
const contractName = compilerMetadata.name || "Unknown Contract";
58+
const abi = compilerMetadata.abi;
59+
60+
// Extract function selector from transaction data (first 4 bytes)
61+
const functionSelector = txParam.data.slice(0, 10) as `0x${string}`;
62+
63+
// Find matching function in ABI
64+
const functions = (abi as readonly AbiItem[]).filter(
65+
(item): item is AbiFunction => item.type === "function",
66+
);
67+
let matchingFunction: AbiFunction | null = null;
68+
69+
for (const func of functions) {
70+
const selector = toFunctionSelector(func);
71+
if (selector === functionSelector) {
72+
matchingFunction = func;
73+
break;
74+
}
75+
}
76+
77+
if (!matchingFunction) {
78+
return null;
79+
}
80+
81+
const functionName = matchingFunction.name;
82+
83+
// Decode function data
84+
const decodedArgs = (await decodeFunctionData({
85+
contract: getContract({
86+
...contract,
87+
abi: [matchingFunction],
88+
}),
89+
data: txParam.data,
90+
})) as readonly unknown[];
91+
92+
// Create a clean object for display
93+
const functionArgs: Record<string, unknown> = {};
94+
if (matchingFunction.inputs && decodedArgs) {
95+
for (let index = 0; index < matchingFunction.inputs.length; index++) {
96+
const input = matchingFunction.inputs[index];
97+
if (input) {
98+
functionArgs[input.name || `arg${index}`] = decodedArgs[index];
99+
}
100+
}
101+
}
102+
103+
return {
104+
contractName,
105+
functionName,
106+
functionArgs,
107+
};
108+
} catch (error) {
109+
console.error("Error decoding transaction param:", error);
110+
return null;
111+
}
112+
}
113+
114+
async function decodeTransactionData(
115+
transaction: Transaction,
116+
): Promise<DecodedTransactionResult> {
117+
try {
118+
// Check if we have transaction parameters
119+
if (
120+
!transaction.transactionParams ||
121+
transaction.transactionParams.length === 0
122+
) {
123+
return [];
124+
}
125+
126+
// Ensure we have a chainId
127+
if (!transaction.chainId) {
128+
return [];
129+
}
130+
131+
const chainId = parseInt(transaction.chainId);
132+
133+
// Decode all transaction parameters in parallel
134+
const decodingPromises = transaction.transactionParams.map((txParam) =>
135+
decodeSingleTransactionParam(txParam, chainId),
136+
);
137+
138+
const results = await Promise.all(decodingPromises);
139+
return results;
140+
} catch (error) {
141+
console.error("Error decoding transaction:", error);
142+
return [];
143+
}
126144
}
127145

128146
export default async function TransactionPage({
129-
params,
147+
params,
130148
}: {
131-
params: Promise<{ team_slug: string; project_slug: string; id: string }>;
149+
params: Promise<{ team_slug: string; project_slug: string; id: string }>;
132150
}) {
133-
const { team_slug, project_slug, id } = await params;
134-
135-
const [authToken, project] = await Promise.all([
136-
getAuthToken(),
137-
getProject(team_slug, project_slug),
138-
]);
139-
140-
if (!authToken) {
141-
loginRedirect(`/team/${team_slug}/${project_slug}/transactions/tx/${id}`);
142-
}
143-
144-
if (!project) {
145-
redirect(`/team/${team_slug}`);
146-
}
147-
148-
const [transactionData, activityLogs] = await Promise.all([
149-
getSingleTransaction({
150-
clientId: project.publishableKey,
151-
teamId: project.teamId,
152-
transactionId: id,
153-
}),
154-
getTransactionActivityLogs({
155-
clientId: project.publishableKey,
156-
teamId: project.teamId,
157-
transactionId: id,
158-
}),
159-
]);
160-
161-
const client = getClientThirdwebClient({
162-
jwt: authToken,
163-
teamId: project.teamId,
164-
});
165-
166-
if (!transactionData) {
167-
notFound();
168-
}
169-
170-
// Decode transaction data on the server
171-
const decodedTransactionData = await decodeTransactionData(transactionData);
172-
173-
return (
174-
<div className="space-y-6 p-2">
175-
<TransactionDetailsUI
176-
activityLogs={activityLogs}
177-
client={client}
178-
project={project}
179-
teamSlug={team_slug}
180-
transaction={transactionData}
181-
decodedTransactionData={decodedTransactionData}
182-
/>
183-
</div>
184-
);
151+
const { team_slug, project_slug, id } = await params;
152+
153+
const [authToken, project] = await Promise.all([
154+
getAuthToken(),
155+
getProject(team_slug, project_slug),
156+
]);
157+
158+
if (!authToken) {
159+
loginRedirect(`/team/${team_slug}/${project_slug}/transactions/tx/${id}`);
160+
}
161+
162+
if (!project) {
163+
redirect(`/team/${team_slug}`);
164+
}
165+
166+
const [transactionData, activityLogs] = await Promise.all([
167+
getSingleTransaction({
168+
clientId: project.publishableKey,
169+
teamId: project.teamId,
170+
transactionId: id,
171+
}),
172+
getTransactionActivityLogs({
173+
clientId: project.publishableKey,
174+
teamId: project.teamId,
175+
transactionId: id,
176+
}),
177+
]);
178+
179+
const client = getClientThirdwebClient({
180+
jwt: authToken,
181+
teamId: project.teamId,
182+
});
183+
184+
if (!transactionData) {
185+
notFound();
186+
}
187+
188+
// Decode transaction data on the server
189+
const decodedTransactionData = await decodeTransactionData(transactionData);
190+
191+
return (
192+
<div className="space-y-6 p-2">
193+
<TransactionDetailsUI
194+
activityLogs={activityLogs}
195+
client={client}
196+
project={project}
197+
teamSlug={team_slug}
198+
transaction={transactionData}
199+
decodedTransactionData={decodedTransactionData}
200+
/>
201+
</div>
202+
);
185203
}

0 commit comments

Comments
 (0)