diff --git a/apps/dashboard/src/@/components/project/create-project-modal/index.tsx b/apps/dashboard/src/@/components/project/create-project-modal/index.tsx index 97ec1dd59b8..708687c227f 100644 --- a/apps/dashboard/src/@/components/project/create-project-modal/index.tsx +++ b/apps/dashboard/src/@/components/project/create-project-modal/index.tsx @@ -67,6 +67,12 @@ const CreateProjectDialog = (props: CreateProjectDialogProps) => { await createVaultAccountAndAccessToken({ project: res.project, projectSecretKey: res.secret, + }).catch((error) => { + console.error( + "Failed to create vault account and access token", + error, + ); + throw error; }); return { project: res.project, diff --git a/apps/dashboard/src/@/hooks/useApi.ts b/apps/dashboard/src/@/hooks/useApi.ts index ca7d4864167..a9218d932e9 100644 --- a/apps/dashboard/src/@/hooks/useApi.ts +++ b/apps/dashboard/src/@/hooks/useApi.ts @@ -2,7 +2,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useActiveAccount } from "thirdweb/react"; import { apiServerProxy } from "@/actions/proxies"; import type { Project } from "@/api/projects"; -import { createVaultAccountAndAccessToken } from "../../app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/lib/vault.client"; +import { rotateVaultAccountAndAccessToken } from "../../app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/lib/vault.client"; import { accountKeys, authorizedWallets } from "../query-keys/cache-keys"; // FIXME: We keep repeating types, API server should provide them @@ -331,12 +331,17 @@ export async function rotateSecretKeyClient(params: { project: Project }) { } try { - // if the project has a vault admin key, rotate it as well - await createVaultAccountAndAccessToken({ - project: params.project, - projectSecretKey: res.data.data.secret, - projectSecretHash: res.data.data.secretHash, - }); + // if the project has an encrypted vault admin key, rotate it as well + const service = params.project.services.find( + (service) => service.name === "engineCloud", + ); + if (service?.encryptedAdminKey) { + await rotateVaultAccountAndAccessToken({ + project: params.project, + projectSecretKey: res.data.data.secret, + projectSecretHash: res.data.data.secretHash, + }); + } } catch (error) { console.error("Failed to rotate vault admin key", error); } diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/analytics/ftux.client.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/analytics/ftux.client.tsx index dca92156fd0..b9e8e581481 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/analytics/ftux.client.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/analytics/ftux.client.tsx @@ -1,11 +1,8 @@ "use client"; -import Link from "next/link"; import { useMemo } from "react"; import type { ThirdwebClient } from "thirdweb"; import type { Project } from "@/api/projects"; import { type Step, StepsCard } from "@/components/blocks/StepsCard"; -import { Button } from "@/components/ui/button"; -import { CreateVaultAccountButton } from "../../vault/components/create-vault-account.client"; import CreateServerWallet from "../server-wallets/components/create-server-wallet.client"; import type { Wallet } from "../server-wallets/wallet-table/types"; import { SendTestTransaction } from "./send-test-tx.client"; @@ -97,27 +94,6 @@ export const EngineChecklist: React.FC = (props) => { ); }; -function CreateVaultAccountStep(props: { - project: Project; - teamSlug: string; - onUserAccessTokenCreated: (userAccessToken: string) => void; -}) { - return ( -
- - -
- ); -} - function CreateServerWalletStep(props: { project: Project; teamSlug: string; diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/analytics/send-test-tx.client.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/analytics/send-test-tx.client.tsx index 701ad1e66d6..6f55f8d3885 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/analytics/send-test-tx.client.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/analytics/send-test-tx.client.tsx @@ -11,6 +11,14 @@ import { engineCloudProxy } from "@/actions/proxies"; import type { Project } from "@/api/projects"; import { SingleNetworkSelector } from "@/components/blocks/NetworkSelectors"; import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Select, @@ -21,6 +29,7 @@ import { } from "@/components/ui/select"; import { useAllChainsData } from "@/hooks/chains/allChains"; import { useDashboardRouter } from "@/lib/DashboardRouter"; +import { TryItOut } from "../server-wallets/components/try-it-out"; import type { Wallet } from "../server-wallets/wallet-table/types"; import { SmartAccountCell } from "../server-wallets/wallet-table/wallet-table-ui.client"; @@ -32,18 +41,17 @@ const formSchema = z.object({ type FormValues = z.infer; -export function SendTestTransaction(props: { +function SendTestTransactionModal(props: { wallets?: Wallet[]; project: Project; teamSlug: string; - expanded?: boolean; walletId?: string; isManagedVault: boolean; client: ThirdwebClient; + open: boolean; + onOpenChange: (open: boolean) => void; }) { const queryClient = useQueryClient(); - const [hasSentTx, setHasSentTx] = useState(false); - const router = useDashboardRouter(); const chainsQuery = useAllChainsData(); @@ -108,6 +116,10 @@ export function SendTestTransaction(props: { }, onSuccess: () => { toast.success("Test transaction sent successfully!"); + // Close the modal after successful transaction + setTimeout(() => { + props.onOpenChange(false); + }, 1000); }, }); @@ -127,147 +139,186 @@ export function SendTestTransaction(props: { queryClient.invalidateQueries({ queryKey: ["transactions", props.project.id], }); - setHasSentTx(true); }; return ( -
+ + + Send a test transaction + + Test your server wallet configuration by sending a transaction + +
- {props.walletId && ( -

Send a test transaction

- )} -

- - Every server wallet action requires your{" "} - {props.isManagedVault ? "project secret key" : "vault access token"}. -

-
- {/* Responsive container */} -
-
-
-

- {props.isManagedVault - ? "Project Secret Key" - : "Vault Access Token"} -

- -

- {props.isManagedVault - ? "Your project secret key was generated when you created your project. If you lost it, you can regenerate one in the project settings." - : "Your vault access token was generated when you created your vault. If you lost it, you can regenerate one in the vault settings."} -

-
+
+

+ + Every server wallet action requires your{" "} + {props.isManagedVault ? "project secret key" : "vault access token"} + . +

+ + {/* Secret Key Input */} +
+

+ {props.isManagedVault + ? "Project Secret Key" + : "Vault Access Token"} +

+ +

+ {props.isManagedVault + ? "Your project secret key was generated when you created your project. If you lost it, you can regenerate one in the project settings." + : "Your vault access token was generated when you created your vault. If you lost it, you can regenerate one in the vault settings."} +

-
-
- {/* Wallet Selector */} -
-
-
-

Server Wallet

- form.setValue("walletIndex", value)} + value={form.watch("walletIndex")} + > + + +
+ + + {selectedWallet.metadata.label} + +
+
+
+ + {props.wallets.map((wallet, index) => ( +
- + - {selectedWallet.metadata.label} + {wallet.metadata.label}
- - - - {props.wallets.map((wallet, index) => ( - -
- - - {wallet.metadata.label} - -
-
- ))} -
- -
-
-

Select Testnet

- - chain.testnet === true && - chain.stackType !== "zksync_stack", - ) - .map((chain) => chain.chainId)} - className="bg-background" - client={props.client} - onChange={(chainId) => { - form.setValue("chainId", chainId); - }} - /> -
+ + ))} + +
-
-
-
- - {hasSentTx && ( + + {/* Testnet Selector */} +
+

Select Testnet

+ + chain.testnet === true && + chain.stackType !== "zksync_stack", + ) + .map((chain) => chain.chainId)} + className="bg-background" + client={props.client} + onChange={(chainId) => { + form.setValue("chainId", chainId); + }} + /> +
+ + {/* Action Buttons */} +
+ - )} +
+ + ); +} + +export function SendTestTransaction(props: { + wallets?: Wallet[]; + project: Project; + teamSlug: string; + expanded?: boolean; + walletId?: string; + isManagedVault: boolean; + client: ThirdwebClient; +}) { + const [isModalOpen, setIsModalOpen] = useState(false); + const router = useDashboardRouter(); + + // Early return in render phase + if (!props.wallets || props.wallets.length === 0) { + return null; + } + + return ( +
+ +
+ + + + + + + +
); } diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/lib/vault.client.ts b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/lib/vault.client.ts index f83e7944617..16b0f96e47d 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/lib/vault.client.ts +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/lib/vault.client.ts @@ -30,6 +30,51 @@ export async function initVaultClient() { return vc; } +export async function rotateVaultAccountAndAccessToken(props: { + project: Project; + projectSecretKey?: string; + projectSecretHash?: string; +}) { + const vaultClient = await initVaultClient(); + const service = props.project.services.find( + (service) => service.name === "engineCloud", + ); + const storedRotationCode = service?.rotationCode; + if (!storedRotationCode) { + throw new Error("No rotation code found"); + } + + const rotateServiceAccountRes = await rotateServiceAccount({ + client: vaultClient, + request: { + auth: { + rotationCode: storedRotationCode, + }, + }, + }); + if (rotateServiceAccountRes.error) { + throw new Error(rotateServiceAccountRes.error.message); + } + const adminKey = rotateServiceAccountRes.data.newAdminKey; + const rotationCode = rotateServiceAccountRes.data.newRotationCode; + + const { managementToken, walletToken } = + await createAndEncryptVaultAccessTokens({ + project: props.project, + projectSecretKey: props.projectSecretKey, + projectSecretHash: props.projectSecretHash, + vaultClient, + adminKey, + rotationCode, + }); + + return { + adminKey, + managementToken, + walletToken, + }; +} + export async function createVaultAccountAndAccessToken(props: { project: Project; projectSecretKey?: string; @@ -38,53 +83,26 @@ export async function createVaultAccountAndAccessToken(props: { try { const vaultClient = await initVaultClient(); - const service = props.project.services.find( - (service) => service.name === "engineCloud", - ); - const storedRotationCode = service?.rotationCode; - const storedEncryptedAdminKey = service?.encryptedAdminKey; - - let adminKey: string | null = null; - let rotationCode: string | null = null; - - if (storedRotationCode && storedEncryptedAdminKey) { - // if the project has a managed vault admin key, rotate it - const rotateServiceAccountRes = await rotateServiceAccount({ - client: vaultClient, - request: { - auth: { - rotationCode: storedRotationCode, + const serviceAccountResult = await createServiceAccount({ + client: vaultClient, + request: { + options: { + metadata: { + projectId: props.project.id, + purpose: "Thirdweb Project Server Wallet Service Account", + teamId: props.project.teamId, }, }, - }); - if (rotateServiceAccountRes.error) { - throw new Error(rotateServiceAccountRes.error.message); - } - adminKey = rotateServiceAccountRes.data.newAdminKey; - rotationCode = rotateServiceAccountRes.data.newRotationCode; - } else { - // otherwise create a new service account - const serviceAccountResult = await createServiceAccount({ - client: vaultClient, - request: { - options: { - metadata: { - projectId: props.project.id, - purpose: "Thirdweb Project Server Wallet Service Account", - teamId: props.project.teamId, - }, - }, - }, - }); - if (serviceAccountResult.success === false) { - throw new Error( - `Failed to create service account: ${serviceAccountResult.error}`, - ); - } - const serviceAccount = serviceAccountResult.data; - adminKey = serviceAccount.adminKey; - rotationCode = serviceAccount.rotationCode; + }, + }); + if (serviceAccountResult.success === false) { + throw new Error( + `Failed to create service account: ${serviceAccountResult.error}`, + ); } + const serviceAccount = serviceAccountResult.data; + const adminKey = serviceAccount.adminKey; + const rotationCode = serviceAccount.rotationCode; const { managementToken, walletToken } = await createAndEncryptVaultAccessTokens({ diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/server-wallets/components/try-it-out.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/server-wallets/components/try-it-out.tsx index 6803b91a94d..984d61da7c0 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/server-wallets/components/try-it-out.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/server-wallets/components/try-it-out.tsx @@ -1,19 +1,17 @@ "use client"; -import { CircleAlertIcon } from "lucide-react"; import Link from "next/link"; import { useState } from "react"; -import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { CodeClient } from "@/components/ui/code/code.client"; import { TabButtons } from "@/components/ui/tabs"; import { NEXT_PUBLIC_ENGINE_CLOUD_URL } from "@/constants/public-envs"; export function TryItOut() { - const [activeTab, setActiveTab] = useState("sdk"); + const [activeTab, setActiveTab] = useState("curl"); return ( -
+ <>
-
+

Usage from your backend @@ -30,16 +28,16 @@ export function TryItOut() { setActiveTab("sdk"), - }, { isActive: activeTab === "curl", name: "Curl", onClick: () => setActiveTab("curl"), }, + { + isActive: activeTab === "sdk", + name: "thirdweb SDK", + onClick: () => setActiveTab("sdk"), + }, { isActive: activeTab === "js", name: "JavaScript", @@ -63,59 +61,10 @@ export function TryItOut() { ]} /> -
+
{activeTab === "sdk" && (
- - - Using the thirdweb SDK on the backend - -

- You can use the full TypeScript thirdweb SDK in your backend, - allowing you to use:{" "} -

    -
  • - The full catalog of{" "} - - extension functions - -
  • -
  • - The{" "} - - prepareContractCall - {" "} - function to encode your transactions -
  • -
  • - The full{" "} - - account - {" "} - interface, predefined chains, and more -
  • -
- The SDK handles encoding your transactions, signing them to - Engine and polling for status. -

-
-

Installation @@ -185,7 +134,7 @@ export function TryItOut() { /> )}

-
+ ); } diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/vault/components/create-vault-account.client.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/vault/components/create-vault-account.client.tsx index 9a494cf8ca1..847895f56b1 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/vault/components/create-vault-account.client.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/vault/components/create-vault-account.client.tsx @@ -78,7 +78,7 @@ export function CreateVaultAccountButton(props: { project: Project }) { await initialiseProjectWithVaultMutation.mutateAsync( manageSelfChecked ? undefined : secretKey, ); - } catch (error) { + } catch { // Error will be handled by the mutation's onError } }; diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/vault/components/rotate-admin-key.client.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/vault/components/rotate-admin-key.client.tsx index 473b2a95255..cc6c700d5d1 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/vault/components/rotate-admin-key.client.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/vault/components/rotate-admin-key.client.tsx @@ -27,8 +27,8 @@ import { Spinner } from "@/components/ui/Spinner/Spinner"; import { useDashboardRouter } from "@/lib/DashboardRouter"; import { cn } from "@/lib/utils"; import { - createVaultAccountAndAccessToken, maskSecret, + rotateVaultAccountAndAccessToken, } from "../../transactions/lib/vault.client"; export default function RotateAdminKeyButton(props: { @@ -43,7 +43,7 @@ export default function RotateAdminKeyButton(props: { const rotateAdminKeyMutation = useMutation({ mutationFn: async () => { // passing no secret key means we're rotating the admin key and deleting any stored keys - const result = await createVaultAccountAndAccessToken({ + const result = await rotateVaultAccountAndAccessToken({ project: props.project, });