Skip to content

Commit 422b4c2

Browse files
add test transaction component
1 parent b58b365 commit 422b4c2

File tree

7 files changed

+302
-18
lines changed

7 files changed

+302
-18
lines changed

apps/dashboard/src/app/team/[team_slug]/[project_slug]/engine/analytics/analytics-page.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { ResponsiveSearchParamsProvider } from "responsive-rsc";
22
import { TransactionAnalyticsFilter } from "./filter";
33
import { TransactionsChartCard } from "./tx-chart/tx-chart";
44
import { TransactionsTable } from "./tx-table/tx-table";
5+
import { SendTestTransaction } from "./send-test-tx.client";
6+
import { Wallet } from "../server-wallets/wallet-table/types";
57

68
export function TransactionsAnalyticsPageContent(props: {
79
searchParams: {
@@ -13,6 +15,7 @@ export function TransactionsAnalyticsPageContent(props: {
1315
clientId: string;
1416
project_slug: string;
1517
team_slug: string;
18+
wallets?: Wallet[];
1619
}) {
1720
return (
1821
<ResponsiveSearchParamsProvider value={props.searchParams}>
@@ -29,6 +32,7 @@ export function TransactionsAnalyticsPageContent(props: {
2932
project_slug={props.project_slug}
3033
team_slug={props.team_slug}
3134
/>
35+
<SendTestTransaction wallets={props.wallets} />
3236
<TransactionsTable teamId={props.teamId} clientId={props.clientId} />
3337
</div>
3438
</div>
Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
"use client";
2+
import { WalletAvatar } from "@/components/blocks/wallet-address";
3+
import { Input } from "@/components/ui/input";
4+
import {
5+
Select,
6+
SelectContent,
7+
SelectItem,
8+
SelectTrigger,
9+
SelectValue,
10+
} from "@/components/ui/select";
11+
import { THIRDWEB_ENGINE_CLOUD_URL } from "@/constants/env";
12+
import { useThirdwebClient } from "@/constants/thirdweb.client";
13+
import { zodResolver } from "@hookform/resolvers/zod";
14+
import { useMutation } from "@tanstack/react-query";
15+
import { ChevronDown, Loader2 } from "lucide-react";
16+
import { useState } from "react";
17+
import { useForm } from "react-hook-form";
18+
import { toast } from "sonner";
19+
import { shortenAddress } from "thirdweb/utils";
20+
import * as z from "zod";
21+
import { SingleNetworkSelector } from "../../../../../../@/components/blocks/NetworkSelectors";
22+
import { Button } from "../../../../../../@/components/ui/button";
23+
import type { Wallet } from "../server-wallets/wallet-table/types";
24+
25+
const formSchema = z.object({
26+
projectSecretKey: z.string().min(1, "Project secret key is required"),
27+
accessToken: z.string().min(1, "Access token is required"),
28+
walletIndex: z.string(),
29+
chainId: z.number(),
30+
});
31+
32+
type FormValues = z.infer<typeof formSchema>;
33+
34+
export function SendTestTransaction(props: {
35+
wallets?: Wallet[];
36+
}) {
37+
const [isOpen, setIsOpen] = useState(false);
38+
const thirdwebClient = useThirdwebClient();
39+
40+
const form = useForm<FormValues>({
41+
resolver: zodResolver(formSchema),
42+
defaultValues: {
43+
projectSecretKey: "",
44+
accessToken: "",
45+
walletIndex: "0",
46+
chainId: 84532,
47+
},
48+
});
49+
50+
const selectedWalletIndex = Number(form.watch("walletIndex"));
51+
const selectedWallet = props.wallets?.[selectedWalletIndex];
52+
53+
const sendDummyTxMutation = useMutation({
54+
mutationFn: async (args: {
55+
walletAddress: string;
56+
accessToken: string;
57+
chainId: number;
58+
}) => {
59+
const response = await fetch(
60+
`${THIRDWEB_ENGINE_CLOUD_URL}/write/transaction`,
61+
{
62+
method: "POST",
63+
headers: {
64+
"Content-Type": "application/json",
65+
"x-secret-key": form.getValues("projectSecretKey"),
66+
},
67+
body: JSON.stringify({
68+
executionOptions: {
69+
type: "AA",
70+
signerAddress: args.walletAddress,
71+
},
72+
transactionParams: [
73+
{
74+
to: args.walletAddress,
75+
value: "0",
76+
},
77+
],
78+
vaultAccessToken: args.accessToken,
79+
chainId: args.chainId.toString(),
80+
}),
81+
},
82+
);
83+
const result = await response.json();
84+
if (!response.ok) {
85+
const errorMsg = result?.error?.message || "Failed to send transaction";
86+
throw new Error(errorMsg);
87+
}
88+
return result;
89+
},
90+
onSuccess: () => {
91+
toast.success("Test transaction sent successfully!");
92+
},
93+
onError: (error) => {
94+
toast.error(error.message);
95+
},
96+
});
97+
98+
const isLoading = sendDummyTxMutation.isPending;
99+
100+
// Early return in render phase
101+
if (!props.wallets || props.wallets.length === 0 || !selectedWallet) {
102+
return null;
103+
}
104+
105+
const onSubmit = (data: FormValues) => {
106+
sendDummyTxMutation.mutate({
107+
walletAddress: selectedWallet.address,
108+
accessToken: data.accessToken,
109+
chainId: data.chainId,
110+
});
111+
};
112+
113+
return (
114+
<div className="w-full space-y-2 rounded-md border p-3">
115+
{/* Trigger Area */}
116+
<div
117+
role="button"
118+
tabIndex={0}
119+
className="flex cursor-pointer items-center justify-between p-3"
120+
onClick={() => setIsOpen(!isOpen)}
121+
onKeyDown={(e) => {
122+
if (e.key === "Enter" || e.key === " ") {
123+
setIsOpen(!isOpen);
124+
}
125+
}}
126+
>
127+
<h2 className="font-semibold text-xl tracking-tight">
128+
Send Test Transaction
129+
</h2>
130+
<ChevronDown
131+
className={`h-4 w-4 transform transition-transform duration-200 ${isOpen ? "rotate-180" : ""}`}
132+
/>
133+
</div>
134+
135+
{/* Content Area (conditional) */}
136+
{isOpen && (
137+
<form
138+
onSubmit={form.handleSubmit(onSubmit)}
139+
className="space-y-4 px-3 pb-3"
140+
>
141+
<p className="text-sm text-warning-text">
142+
🔐 This action requires a project secret key and a vault access
143+
token.
144+
</p>
145+
{/* Responsive container */}
146+
<div className="flex flex-col gap-2 md:flex-row md:items-end md:gap-2">
147+
<div className="flex-grow">
148+
<div className="flex flex-col gap-2">
149+
<p className="text-sm">Project Secret Key</p>
150+
<Input
151+
id="secret-key"
152+
type="password"
153+
placeholder="Project Secret Key"
154+
{...form.register("projectSecretKey")}
155+
disabled={isLoading}
156+
className="text-xs"
157+
/>
158+
</div>
159+
</div>
160+
<div className="flex-grow">
161+
<div className="flex flex-col gap-2">
162+
<p className="text-sm">Vault Access Token</p>
163+
<Input
164+
id="access-token"
165+
type="password"
166+
placeholder="Vault Access Token"
167+
{...form.register("accessToken")}
168+
disabled={isLoading}
169+
className="text-xs"
170+
/>
171+
</div>
172+
</div>
173+
</div>
174+
{/* Wallet Selector */}
175+
<div className="flex flex-col gap-2">
176+
<div className="flex flex-col gap-2 md:flex-row md:items-end md:gap-2">
177+
<div className="flex flex-1 flex-col gap-2">
178+
<p className="text-sm">Signer</p>
179+
<Select
180+
value={form.watch("walletIndex")}
181+
onValueChange={(value) => form.setValue("walletIndex", value)}
182+
>
183+
<SelectTrigger className="w-full">
184+
<SelectValue>
185+
<div className="flex items-center gap-2">
186+
<WalletAvatar
187+
address={selectedWallet.address}
188+
profiles={[]}
189+
thirdwebClient={thirdwebClient}
190+
iconClassName="h-5 w-5"
191+
/>
192+
<span className="font-mono text-sm">
193+
{shortenAddress(selectedWallet.address)}
194+
</span>
195+
</div>
196+
</SelectValue>
197+
</SelectTrigger>
198+
<SelectContent>
199+
{props.wallets.map((wallet, index) => (
200+
<SelectItem key={wallet.address} value={index.toString()}>
201+
<div className="flex items-center gap-2">
202+
<WalletAvatar
203+
address={wallet.address}
204+
profiles={[]}
205+
thirdwebClient={thirdwebClient}
206+
iconClassName="h-5 w-5"
207+
/>
208+
<span className="font-mono text-sm">
209+
{shortenAddress(wallet.address)}
210+
</span>
211+
</div>
212+
</SelectItem>
213+
))}
214+
</SelectContent>
215+
</Select>
216+
</div>
217+
<div className="flex flex-1 flex-col gap-2">
218+
<p className="text-sm">Network</p>
219+
<SingleNetworkSelector
220+
chainId={form.watch("chainId")}
221+
onChange={(chainId) => {
222+
form.setValue("chainId", chainId);
223+
}}
224+
/>
225+
</div>
226+
</div>
227+
</div>
228+
<div className="flex justify-end">
229+
<Button
230+
type="submit"
231+
variant="primary"
232+
disabled={isLoading}
233+
size="sm"
234+
className="w-full min-w-[200px] md:w-auto"
235+
>
236+
{isLoading ? (
237+
<>
238+
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
239+
Sending...
240+
</>
241+
) : (
242+
"Send Transaction"
243+
)}
244+
</Button>
245+
</div>
246+
</form>
247+
)}
248+
</div>
249+
);
250+
}

apps/dashboard/src/app/team/[team_slug]/[project_slug]/engine/page.tsx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import { notFound, redirect } from "next/navigation";
44
import { getAuthToken } from "../../../../api/lib/getAuthToken";
55
import { TransactionsAnalyticsPageContent } from "./analytics/analytics-page";
66
import { TransactionAnalyticsSummary } from "./analytics/summary";
7+
import { createVaultClient, listEoas } from "@thirdweb-dev/vault-sdk";
8+
import { THIRDWEB_VAULT_URL } from "../../../../../@/constants/env";
9+
import { Wallet } from "./server-wallets/wallet-table/types";
710

811
export default async function TransactionsAnalyticsPage(props: {
912
params: Promise<{ team_slug: string; project_slug: string }>;
@@ -36,6 +39,31 @@ export default async function TransactionsAnalyticsPage(props: {
3639
redirect(`/team/${params.team_slug}`);
3740
}
3841

42+
const projectEngineCloudService = project.services.find(
43+
(service) => service.name === "engineCloud",
44+
);
45+
46+
const vaultClient = await createVaultClient({
47+
baseUrl: THIRDWEB_VAULT_URL,
48+
});
49+
50+
const managementAccessToken =
51+
projectEngineCloudService?.managementAccessToken;
52+
53+
const eoas = managementAccessToken
54+
? await listEoas({
55+
client: vaultClient,
56+
request: {
57+
auth: {
58+
accessToken: managementAccessToken,
59+
},
60+
options: {},
61+
},
62+
})
63+
: { data: { items: [] }, error: null, success: true };
64+
65+
const wallets = eoas.data?.items as Wallet[] | undefined;
66+
3967
return (
4068
<div className="flex grow flex-col">
4169
<TransactionAnalyticsSummary
@@ -49,6 +77,7 @@ export default async function TransactionsAnalyticsPage(props: {
4977
clientId={project.publishableKey}
5078
project_slug={params.project_slug}
5179
team_slug={params.team_slug}
80+
wallets={wallets}
5281
/>
5382
</div>
5483
);

apps/dashboard/src/app/team/[team_slug]/[project_slug]/engine/server-wallets/components/list-access-tokens.client.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ import { Input } from "@/components/ui/input";
1313
import { useMutation, useQuery } from "@tanstack/react-query";
1414
import {
1515
createAccessToken,
16-
revokeAccessToken,
1716
listAccessTokens,
17+
revokeAccessToken,
1818
} from "@thirdweb-dev/vault-sdk";
1919
import { createVaultClient } from "@thirdweb-dev/vault-sdk";
2020
import { Loader2, Trash2 } from "lucide-react";
@@ -396,7 +396,7 @@ export default function ListAccessTokensButton(props: {
396396
<div className="px-6 pb-6">
397397
<div className="flex flex-col gap-4">
398398
<p className="text-sm text-warning-text">
399-
This action requires your Vault admin key.
399+
🔐 This action requires your Vault admin key.
400400
</p>
401401
<Input
402402
type="password"

apps/dashboard/src/app/team/[team_slug]/[project_slug]/engine/server-wallets/components/rotate-admin-key.client.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ export default function RotateAdminKeyButton() {
192192
<div className="px-6 pb-6">
193193
<div className="flex flex-col gap-4">
194194
<p className="text-sm text-warning-text">
195-
This action requires your Vault rotation code.
195+
🔐 This action requires your Vault rotation code.
196196
</p>
197197
<Input
198198
type="password"

apps/dashboard/src/app/team/[team_slug]/[project_slug]/engine/server-wallets/components/try-it-out.tsx

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import { Button } from "@/components/ui/button";
2-
import { CodeServer } from "../../../../../../../@/components/ui/code/code.server";
3-
import { THIRDWEB_ENGINE_CLOUD_URL } from "../../../../../../../@/constants/env";
1+
import { CodeServer } from "@/components/ui/code/code.server";
2+
import { THIRDWEB_ENGINE_CLOUD_URL } from "@/constants/env";
3+
import Link from "next/link";
4+
import { Button } from "../../../../../../../@/components/ui/button";
45
import type { Wallet } from "../wallet-table/types";
5-
import SendDummyTx from "./send-dummy-tx.client";
66

77
export function TryItOut(props: {
88
authToken: string;
@@ -33,21 +33,22 @@ export function TryItOut(props: {
3333
<div className="h-4" />
3434
<div className="flex flex-row justify-end gap-4">
3535
<Button variant={"secondary"} asChild>
36-
<a
37-
href={`${THIRDWEB_ENGINE_CLOUD_URL}/reference`}
38-
target="_blank"
36+
<Link
37+
href={`/team/${props.team_slug}/${props.project_slug}/engine/explorer`}
3938
rel="noreferrer"
4039
>
4140
View API reference
42-
</a>
41+
</Link>
4342
</Button>
4443
{props.wallet && (
45-
<SendDummyTx
46-
authToken={props.authToken}
47-
wallet={props.wallet}
48-
team_slug={props.team_slug}
49-
project_slug={props.project_slug}
50-
/>
44+
<Button variant={"primary"} asChild>
45+
<Link
46+
href={`/team/${props.team_slug}/${props.project_slug}/engine`}
47+
rel="noreferrer"
48+
>
49+
Send a test transaction
50+
</Link>
51+
</Button>
5152
)}
5253
</div>
5354
</div>

0 commit comments

Comments
 (0)