From 6a7b556d75dd7bd5152250c6625e85dc047cf249 Mon Sep 17 00:00:00 2001 From: MananTank Date: Wed, 22 Oct 2025 16:32:58 +0000 Subject: [PATCH] [BLD-389] Dashboard: Project Wallet section UI improvements and refactor (#8291) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ## PR-Codex overview This PR primarily focuses on refactoring and improving the handling of project wallets within the application. It removes unused components, simplifies props, and enhances the user interface for wallet management. ### Detailed summary - Deleted unused files: `ProjectWalletSetup.client.tsx` and `ProjectWalletControls.client.tsx`. - Updated `ProjectFTUX` to remove `managementAccessToken` prop. - Simplified `CreateSolanaWallet` and `CreateServerWallet` components by removing unnecessary props. - Enhanced styling in the `ProjectFTUX` story and the wallet-related components. - Improved state management for wallet selection and transactions. - Refactored `ProjectWalletSection` to streamline wallet creation and management processes. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` ## Summary by CodeRabbit * **New Features** * Added comprehensive project wallet management interface with send, receive, view transactions, and change wallet actions through an interactive dashboard. * **Refactor** * Reorganized wallet functionality into a dedicated module for improved code organization. * Simplified component interfaces by removing redundant authentication parameters throughout the dashboard. --- .../@/components/blocks/wallet-address.tsx | 17 +- .../ProjectFTUX/ProjectFTUX.stories.tsx | 8 +- .../components/ProjectFTUX/ProjectFTUX.tsx | 81 +- .../ProjectWalletControls.client.tsx | 747 ------------------ .../ProjectFTUX/ProjectWalletSetup.client.tsx | 215 ----- .../project-wallet/project-wallet-details.tsx | 736 +++++++++++++++++ .../project-wallet/project-wallet.stories.tsx | 126 +++ .../project-wallet/project-wallet.tsx | 331 ++++++++ .../[project_slug]/(sidebar)/page.tsx | 31 +- .../transactions/analytics/ftux.client.tsx | 31 +- .../server-wallets-table.client.tsx | 11 +- .../(sidebar)/transactions/page.tsx | 2 - .../create-server-wallet.client.tsx | 37 +- .../create-solana-wallet.client.tsx | 9 +- .../[project_slug]/(sidebar)/wallets/page.tsx | 1 - 15 files changed, 1261 insertions(+), 1122 deletions(-) delete mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/ProjectFTUX/ProjectWalletControls.client.tsx delete mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/ProjectFTUX/ProjectWalletSetup.client.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/project-wallet/project-wallet-details.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/project-wallet/project-wallet.stories.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/project-wallet/project-wallet.tsx diff --git a/apps/dashboard/src/@/components/blocks/wallet-address.tsx b/apps/dashboard/src/@/components/blocks/wallet-address.tsx index 82db53e6476..163c9b95ce9 100644 --- a/apps/dashboard/src/@/components/blocks/wallet-address.tsx +++ b/apps/dashboard/src/@/components/blocks/wallet-address.tsx @@ -51,17 +51,12 @@ export function WalletAddressUI( }; }, ) { - // default back to zero address if no address provided - const address = useMemo(() => props.address || ZERO_ADDRESS, [props.address]); - - const [shortenedAddress, _lessShortenedAddress] = useMemo(() => { - return [ - props.shortenAddress !== false - ? `${address.slice(0, 6)}...${address.slice(-4)}` - : address, - `${address.slice(0, 14)}...${address.slice(-12)}`, - ]; - }, [address, props.shortenAddress]); + const address = props.address || ZERO_ADDRESS; + + const shortenedAddress = + props.shortenAddress !== false + ? `${address.slice(0, 6)}...${address.slice(-4)}` + : address; if (!isAddress(address)) { return ( diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/ProjectFTUX/ProjectFTUX.stories.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/ProjectFTUX/ProjectFTUX.stories.tsx index ec7a9704b41..e48d6d82575 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/ProjectFTUX/ProjectFTUX.stories.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/ProjectFTUX/ProjectFTUX.stories.tsx @@ -4,9 +4,14 @@ import { ProjectFTUX } from "./ProjectFTUX"; const meta = { component: ProjectFTUX, + parameters: { + nextjs: { + appDirectory: true, + }, + }, decorators: [ (Story) => ( -
+
), @@ -31,6 +36,5 @@ export const Default: Story = { ], }, teamSlug: "bar", - managementAccessToken: undefined, }, }; diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/ProjectFTUX/ProjectFTUX.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/ProjectFTUX/ProjectFTUX.tsx index 144415b5441..f780934040f 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/ProjectFTUX/ProjectFTUX.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/ProjectFTUX/ProjectFTUX.tsx @@ -15,33 +15,12 @@ import { UnrealIcon } from "@/icons/brand-icons/UnrealIcon"; import { ContractIcon } from "@/icons/ContractIcon"; import { InsightIcon } from "@/icons/InsightIcon"; import { PayIcon } from "@/icons/PayIcon"; -import { WalletProductIcon } from "@/icons/WalletProductIcon"; -import { getProjectWalletLabel } from "@/lib/project-wallet"; -import { - getProjectWallet, - type ProjectWalletSummary, -} from "@/lib/server/project-wallet"; import { ClientIDSection } from "./ClientIDSection"; -import { ProjectWalletControls } from "./ProjectWalletControls.client"; -import { ProjectWalletSetup } from "./ProjectWalletSetup.client"; import { SecretKeySection } from "./SecretKeySection"; -export async function ProjectFTUX(props: { - project: Project; - teamSlug: string; - wallet?: ProjectWalletSummary | undefined; - managementAccessToken: string | undefined; -}) { - const projectWallet = props.wallet ?? (await getProjectWallet(props.project)); - +export function ProjectFTUX(props: { project: Project; teamSlug: string }) { return (
- -
-
-
-
- -
-
-

- Project Wallet -

-

- Use it for deployments, payments, and API integrations. -

-
-
- - {walletAddress ? ( - <> - -
- - View Transactions - - -
- - ) : ( - - )} -
-
- - ); -} - // Integrate API key section ------------------------------------------------------------ function IntegrateAPIKeySection({ project }: { project: Project }) { diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/ProjectFTUX/ProjectWalletControls.client.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/ProjectFTUX/ProjectWalletControls.client.tsx deleted file mode 100644 index eb9ea652eba..00000000000 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/ProjectFTUX/ProjectWalletControls.client.tsx +++ /dev/null @@ -1,747 +0,0 @@ -"use client"; - -import { zodResolver } from "@hookform/resolvers/zod"; -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { - EllipsisVerticalIcon, - RefreshCcwIcon, - SendIcon, - ShuffleIcon, - WalletIcon, -} from "lucide-react"; -import { useEffect, useMemo, useState } from "react"; -import { useForm } from "react-hook-form"; -import { toast } from "sonner"; -import { toWei } from "thirdweb"; -import { useWalletBalance } from "thirdweb/react"; -import { isAddress } from "thirdweb/utils"; -import { z } from "zod"; -import { listProjectServerWallets } from "@/actions/project-wallet/list-server-wallets"; -import { sendProjectWalletTokens } from "@/actions/project-wallet/send-tokens"; -import type { Project } from "@/api/project/projects"; -import { FundWalletModal } from "@/components/blocks/fund-wallets-modal"; -import { SingleNetworkSelector } from "@/components/blocks/NetworkSelectors"; -import { WalletAddress } from "@/components/blocks/wallet-address"; -import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; -import { - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; -import { Spinner } from "@/components/ui/Spinner"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { getClientThirdwebClient } from "@/constants/thirdweb-client.client"; -import { useV5DashboardChain } from "@/hooks/chains/v5-adapter"; -import { useDashboardRouter } from "@/lib/DashboardRouter"; -import type { ProjectWalletSummary } from "@/lib/server/project-wallet"; -import { cn } from "@/lib/utils"; -import { updateDefaultProjectWallet } from "../../transactions/lib/vault.client"; - -type ProjectWalletControlsProps = { - walletAddress: string; - label: string; - project: Pick< - Project, - "id" | "publishableKey" | "teamId" | "services" | "name" - >; - defaultChainId?: number; -}; - -export function ProjectWalletControls(props: ProjectWalletControlsProps) { - const { walletAddress, label, project, defaultChainId } = props; - const [isSendOpen, setIsSendOpen] = useState(false); - const [isReceiveOpen, setIsReceiveOpen] = useState(false); - const [selectedChainId, setSelectedChainId] = useState(defaultChainId ?? 1); - const [isChangeWalletOpen, setIsChangeWalletOpen] = useState(false); - const [selectedWalletId, setSelectedWalletId] = useState( - undefined, - ); - - const client = useMemo(() => getClientThirdwebClient(), []); - const chain = useV5DashboardChain(selectedChainId); - const queryClient = useQueryClient(); - const router = useDashboardRouter(); - - const engineCloudService = useMemo( - () => project.services?.find((service) => service.name === "engineCloud"), - [project.services], - ); - const managementAccessToken = - engineCloudService?.managementAccessToken ?? undefined; - const isManagedVault = !!engineCloudService?.encryptedAdminKey; - - const serverWalletsQuery = useQuery({ - enabled: !!managementAccessToken, - queryFn: async () => { - if (!managementAccessToken) { - return [] as ProjectWalletSummary[]; - } - - return listProjectServerWallets({ - managementAccessToken, - projectId: project.id, - }); - }, - queryKey: ["project", project.id, "server-wallets", managementAccessToken], - staleTime: 60_000, - }); - - const serverWallets = serverWalletsQuery.data ?? []; - const currentWalletAddressLower = walletAddress.toLowerCase(); - const otherWallets = useMemo(() => { - return serverWallets.filter((wallet) => { - return wallet.address.toLowerCase() !== currentWalletAddressLower; - }); - }, [serverWallets, currentWalletAddressLower]); - - const canChangeWallet = otherWallets.length > 0; - - const handleOpenChangeWallet = () => { - const currentWallet = serverWallets.find( - (wallet) => wallet.address.toLowerCase() === currentWalletAddressLower, - ); - - const nextWalletId = - otherWallets[0]?.id ?? currentWallet?.id ?? serverWallets[0]?.id; - - setSelectedWalletId(nextWalletId); - setIsChangeWalletOpen(true); - }; - - const handleCloseChangeWallet = () => { - setIsChangeWalletOpen(false); - setSelectedWalletId(undefined); - }; - - const selectedWallet = useMemo(() => { - if (!selectedWalletId) { - return undefined; - } - - return serverWallets.find((wallet) => wallet.id === selectedWalletId); - }, [selectedWalletId, serverWallets]); - - const isSelectionDifferent = Boolean( - selectedWallet && - selectedWallet.address.toLowerCase() !== currentWalletAddressLower, - ); - - const changeWalletMutation = useMutation({ - mutationFn: async (wallet: ProjectWalletSummary) => { - await updateDefaultProjectWallet({ - project: project as Project, - projectWalletAddress: wallet.address, - }); - }, - onError: (error) => { - const message = - error instanceof Error - ? error.message - : "Failed to update project wallet"; - toast.error(message); - }, - onSuccess: async (_, wallet) => { - const descriptionLabel = wallet.label ?? wallet.address; - toast.success("Project wallet updated", { - description: `Now pointing to ${descriptionLabel}`, - }); - await queryClient.invalidateQueries({ - queryKey: [ - "project", - project.id, - "server-wallets", - managementAccessToken, - ], - }); - handleCloseChangeWallet(); - router.refresh(); - }, - }); - - const balanceQuery = useWalletBalance({ - address: walletAddress, - chain, - client, - }); - - const balanceDisplay = balanceQuery.data - ? `${balanceQuery.data.displayValue} ${balanceQuery.data.symbol}` - : undefined; - - return ( -
-
-
-
-
-

Address

- -
-
-

Label

-
-

- {label} -

- {canChangeWallet && ( - - )} -
-
-
- -
-
-

Balance

-
- - {balanceQuery.isLoading ? ( - - ) : balanceDisplay ? ( - - {balanceDisplay} - - ) : ( - N/A - )} - - - - - - - setIsSendOpen(true)} - > - - Send funds - - setIsReceiveOpen(true)} - > - - Receive funds - - - -
-
-
-

Network

- -
-
-
-
- - setIsSendOpen(false)} - onSuccess={() => balanceQuery.refetch()} - open={isSendOpen} - publishableKey={project.publishableKey} - teamId={project.teamId} - walletAddress={walletAddress} - /> - - - - { - if (!nextOpen) { - handleCloseChangeWallet(); - } - }} - open={isChangeWalletOpen} - > - - - Change default wallet - - Choose another server wallet to use as the default for this - project. - - - -
- {serverWalletsQuery.isLoading ? ( -
- -
- ) : serverWallets.length <= 1 ? ( -

- You need at least two server wallets to pick a different - default. -

- ) : ( -
- -
- )} -
- - - - - -
-
-
- ); -} - -const createSendFormSchema = (secretKeyLabel: string) => - z.object({ - chainId: z.number({ - required_error: "Select a network", - }), - toAddress: z - .string() - .trim() - .min(1, "Destination address is required") - .refine((value) => Boolean(isAddress(value)), { - message: "Enter a valid wallet address", - }), - amount: z.string().trim().min(1, "Amount is required"), - secretKey: z.string().trim().min(1, `${secretKeyLabel} is required`), - vaultAccessToken: z.string().trim(), - }); - -const SECRET_KEY_LABEL = "Project secret key"; - -type SendFormValues = z.infer>; - -function SendProjectWalletModal(props: { - open: boolean; - onClose: () => void; - onSuccess: () => void; - walletAddress: string; - publishableKey: string; - teamId: string; - chainId: number; - label: string; - client: ReturnType; - isManagedVault: boolean; -}) { - const { - open, - onClose, - onSuccess, - walletAddress, - publishableKey, - teamId, - chainId, - label, - client, - isManagedVault, - } = props; - - const secretKeyLabel = SECRET_KEY_LABEL; - const secretKeyPlaceholder = "Enter your project secret key"; - const secretKeyHelper = - "Your project secret key was generated when you created your project. If you lost it, regenerate one from Project settings."; - const vaultAccessTokenHelper = - "Vault access tokens are optional credentials with server wallet permissions. Manage them in Vault settings."; - - const formSchema = useMemo(() => createSendFormSchema(SECRET_KEY_LABEL), []); - - const form = useForm({ - defaultValues: { - amount: "0", - chainId, - secretKey: "", - vaultAccessToken: "", - toAddress: "", - }, - mode: "onChange", - resolver: zodResolver(formSchema), - }); - - const selectedChain = useV5DashboardChain(form.watch("chainId")); - - // eslint-disable-next-line no-restricted-syntax -- form submission chainId must track selector state - useEffect(() => { - form.setValue("chainId", chainId); - }, [chainId, form]); - - // eslint-disable-next-line no-restricted-syntax -- reset cached inputs when modal closes to avoid leaking state - useEffect(() => { - if (!open) { - const currentValues = form.getValues(); - form.reset({ - amount: "0", - chainId, - secretKey: currentValues.secretKey ?? "", - vaultAccessToken: currentValues.vaultAccessToken ?? "", - toAddress: "", - }); - } - }, [open, chainId, form]); - - const sendMutation = useMutation({ - mutationFn: async (values: SendFormValues) => { - const quantityWei = toWei(values.amount).toString(); - const secretKeyValue = values.secretKey.trim(); - const vaultAccessTokenValue = values.vaultAccessToken.trim(); - - const result = await sendProjectWalletTokens({ - chainId: values.chainId, - publishableKey, - quantityWei, - recipientAddress: values.toAddress, - teamId, - walletAddress, - secretKey: secretKeyValue, - ...(vaultAccessTokenValue - ? { vaultAccessToken: vaultAccessTokenValue } - : {}), - }); - - if (!result.ok) { - const errorMessage = - typeof result.error === "string" - ? result.error - : "Failed to send funds"; - throw new Error(errorMessage); - } - - return result.transactionIds; - }, - onError: (error) => { - toast.error(error.message); - }, - onSuccess: (transactionIds) => { - toast.success("Transfer submitted", { - description: - transactionIds && transactionIds.length > 0 - ? `Transaction ID: ${transactionIds[0]}` - : undefined, - }); - onSuccess(); - onClose(); - }, - }); - - return ( - { - if (!nextOpen) { - onClose(); - } - }} - open={open} - > - - - Send from {label} - - Execute a one-off transfer using your server wallet. - - - -
- { - void sendMutation.mutateAsync(values); - })} - > -
- ( - - Network - - { - field.onChange(nextChainId); - }} - placeholder="Select network" - /> - - - - )} - /> - - ( - - Send to - - - - - - )} - /> - - ( - - Amount - - - - - Sending native token - {selectedChain?.nativeCurrency?.symbol - ? ` (${selectedChain.nativeCurrency.symbol})` - : ""} - - - - )} - /> - - ( - - {secretKeyLabel} - - - - {secretKeyHelper} - - - )} - /> - - {!isManagedVault && ( - ( - - - Vault access token - - {" "} - (optional) - - - - - - - {vaultAccessTokenHelper} - - - - )} - /> - )} -
- -
- - -
-
- -
-
- ); -} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/ProjectFTUX/ProjectWalletSetup.client.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/ProjectFTUX/ProjectWalletSetup.client.tsx deleted file mode 100644 index e04a35fc92f..00000000000 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/ProjectFTUX/ProjectWalletSetup.client.tsx +++ /dev/null @@ -1,215 +0,0 @@ -"use client"; - -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { CircleAlertIcon } from "lucide-react"; -import { useMemo, useState } from "react"; -import { toast } from "sonner"; -import { listProjectServerWallets } from "@/actions/project-wallet/list-server-wallets"; -import type { Project } from "@/api/project/projects"; -import { WalletAddress } from "@/components/blocks/wallet-address"; -import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; -import { Button } from "@/components/ui/button"; -import { Spinner } from "@/components/ui/Spinner"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { getClientThirdwebClient } from "@/constants/thirdweb-client.client"; -import { useDashboardRouter } from "@/lib/DashboardRouter"; -import type { ProjectWalletSummary } from "@/lib/server/project-wallet"; -import { updateDefaultProjectWallet } from "../../transactions/lib/vault.client"; -import CreateServerWallet from "../../transactions/server-wallets/components/create-server-wallet.client"; - -export function ProjectWalletSetup(props: { - project: Project; - teamSlug: string; - managementAccessToken: string | undefined; -}) { - const { project, teamSlug, managementAccessToken } = props; - const router = useDashboardRouter(); - const queryClient = useQueryClient(); - const client = useMemo(() => getClientThirdwebClient(), []); - const [selectedWalletId, setSelectedWalletId] = useState( - undefined, - ); - - const serverWalletsQuery = useQuery({ - enabled: Boolean(managementAccessToken), - queryFn: async () => { - if (!managementAccessToken) { - return [] as ProjectWalletSummary[]; - } - - return listProjectServerWallets({ - managementAccessToken, - projectId: project.id, - }); - }, - queryKey: ["project", project.id, "server-wallets", managementAccessToken], - staleTime: 60_000, - }); - - const serverWallets = serverWalletsQuery.data ?? []; - - const effectiveSelectedWalletId = useMemo(() => { - if ( - selectedWalletId && - serverWallets.some((wallet) => wallet.id === selectedWalletId) - ) { - return selectedWalletId; - } - - return serverWallets[0]?.id; - }, [selectedWalletId, serverWallets]); - - const selectedWallet = useMemo(() => { - if (!effectiveSelectedWalletId) { - return undefined; - } - - return serverWallets.find( - (wallet) => wallet.id === effectiveSelectedWalletId, - ); - }, [effectiveSelectedWalletId, serverWallets]); - - const setDefaultWalletMutation = useMutation({ - mutationFn: async (wallet: ProjectWalletSummary) => { - await updateDefaultProjectWallet({ - project, - projectWalletAddress: wallet.address, - }); - }, - onError: (error) => { - const message = - error instanceof Error - ? error.message - : "Failed to update project wallet"; - toast.error(message); - }, - onSuccess: async (_, wallet) => { - const descriptionLabel = wallet.label ?? wallet.address; - toast.success("Project wallet updated", { - description: `Now pointing to ${descriptionLabel}`, - }); - await queryClient.invalidateQueries({ - queryKey: [ - "project", - project.id, - "server-wallets", - managementAccessToken, - ], - }); - router.refresh(); - }, - }); - - const canSelectExisting = - Boolean(managementAccessToken) && serverWallets.length > 0; - - return ( - - - No project wallet set - - Set a project wallet to use for dashboard and API integrations. - - -
- {serverWalletsQuery.isLoading ? ( -
- - Loading existing server wallets… -
- ) : canSelectExisting ? ( -
-
-

- Choose an existing server wallet -

-

- These wallets were already created for this project. Pick one to - set as the default. -

- -
- -
- - -
-
- ) : ( -
-

- Create a server wallet to start using transactions and other - project features. -

- -
- )} -
-
- ); -} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/project-wallet/project-wallet-details.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/project-wallet/project-wallet-details.tsx new file mode 100644 index 00000000000..819d0c652f4 --- /dev/null +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/project-wallet/project-wallet-details.tsx @@ -0,0 +1,736 @@ +"use client"; + +import { zodResolver } from "@hookform/resolvers/zod"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { + ArrowLeftRightIcon, + EllipsisVerticalIcon, + RefreshCcwIcon, + SendIcon, + ShuffleIcon, + WalletIcon, +} from "lucide-react"; +import { useMemo, useState } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { type ThirdwebClient, toWei } from "thirdweb"; +import { useWalletBalance } from "thirdweb/react"; +import { isAddress } from "thirdweb/utils"; +import { z } from "zod"; +import { sendProjectWalletTokens } from "@/actions/project-wallet/send-tokens"; +import type { Project } from "@/api/project/projects"; +import { FundWalletModal } from "@/components/blocks/fund-wallets-modal"; +import { SingleNetworkSelector } from "@/components/blocks/NetworkSelectors"; +import { WalletAddress } from "@/components/blocks/wallet-address"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { CopyTextButton } from "@/components/ui/CopyTextButton"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { Spinner } from "@/components/ui/Spinner"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Skeleton } from "@/components/ui/skeleton"; +import type { getClientThirdwebClient } from "@/constants/thirdweb-client.client"; +import { useV5DashboardChain } from "@/hooks/chains/v5-adapter"; +import { useDashboardRouter } from "@/lib/DashboardRouter"; +import type { ProjectWalletSummary } from "@/lib/server/project-wallet"; +import { cn } from "@/lib/utils"; +import { updateDefaultProjectWallet } from "../../transactions/lib/vault.client"; + +type GetProjectServerWallets = (params: { + managementAccessToken: string; + projectId: string; +}) => Promise; + +type ProjectWalletControlsProps = { + projectWallet: ProjectWalletSummary; + project: Project; + defaultChainId?: number; + teamSlug: string; + getProjectServerWallets: GetProjectServerWallets; + client: ThirdwebClient; +}; + +export function ProjectWalletDetailsSection(props: ProjectWalletControlsProps) { + const { projectWallet, project, defaultChainId } = props; + const [isSendOpen, setIsSendOpen] = useState(false); + const [isReceiveOpen, setIsReceiveOpen] = useState(false); + const [selectedChainId, setSelectedChainId] = useState(defaultChainId ?? 1); + const [isChangeWalletOpen, setIsChangeWalletOpen] = useState(false); + + const chain = useV5DashboardChain(selectedChainId); + + const engineCloudService = useMemo( + () => project.services?.find((service) => service.name === "engineCloud"), + [project.services], + ); + + const managementAccessToken = + engineCloudService?.managementAccessToken ?? undefined; + + const isManagedVault = !!engineCloudService?.encryptedAdminKey; + + const serverWalletsQuery = useQuery({ + enabled: !!managementAccessToken, + queryFn: async () => { + if (!managementAccessToken) { + return [] as ProjectWalletSummary[]; + } + + return props.getProjectServerWallets({ + managementAccessToken, + projectId: project.id, + }); + }, + queryKey: ["project", project.id, "server-wallets", managementAccessToken], + staleTime: 60_000, + }); + + const balanceQuery = useWalletBalance({ + address: projectWallet.address, + chain, + client: props.client, + }); + + const canChangeWallet = + serverWalletsQuery.data && serverWalletsQuery.data.length > 1; + + const router = useDashboardRouter(); + + return ( +
+
+ + + + + + setIsSendOpen(true)} + > + + Send funds + + setIsReceiveOpen(true)} + > + + Receive funds + + + + router.push( + `/team/${props.teamSlug}/${props.project.slug}/transactions`, + ) + } + > + + View transactions + + + {canChangeWallet && ( + setIsChangeWalletOpen(true)} + > + + Change project wallet + + )} + + + +
+
+

Wallet Address

+ +
+ +
+

Wallet Label

+
+

+ {projectWallet.label || "N/A"} +

+
+
+
+ +
+
+

Balance

+
+ {balanceQuery.isFetching || !balanceQuery.data ? ( + + ) : ( + + {balanceQuery.data.displayValue} {balanceQuery.data.symbol} + + )} + + +
+
+ + +
+
+ + setIsSendOpen(false)} + onSuccess={() => balanceQuery.refetch()} + open={isSendOpen} + publishableKey={project.publishableKey} + teamId={project.teamId} + walletAddress={projectWallet.address} + /> + + + + + + + + +
+ ); +} + +function ChangeProjectWalletDialogContent(props: { + isOpen: boolean; + setIsOpen: (isOpen: boolean) => void; + serverWallets: { + data: ProjectWalletSummary[]; + isPending: boolean; + }; + projectWallet: ProjectWalletSummary; + managementAccessToken: string | undefined; + project: Project; + client: ThirdwebClient; +}) { + const queryClient = useQueryClient(); + const router = useDashboardRouter(); + + const [selectedWalletId, setSelectedWalletId] = useState( + props.projectWallet.id, + ); + + const serverWallets = props.serverWallets.data; + + const currentWalletAddressLower = props.projectWallet.address.toLowerCase(); + + const selectedWallet = useMemo(() => { + if (!selectedWalletId) { + return undefined; + } + + return serverWallets.find((wallet) => wallet.id === selectedWalletId); + }, [selectedWalletId, serverWallets]); + + const isSelectionDifferent = Boolean( + selectedWallet && + selectedWallet.address.toLowerCase() !== currentWalletAddressLower, + ); + + const changeWalletMutation = useMutation({ + mutationFn: async (wallet: ProjectWalletSummary) => { + await updateDefaultProjectWallet({ + project: props.project, + projectWalletAddress: wallet.address, + }); + }, + onError: (error) => { + const message = + error instanceof Error + ? error.message + : "Failed to update project wallet"; + toast.error(message); + }, + onSuccess: async () => { + toast.success("Project wallet updated"); + await queryClient.invalidateQueries({ + queryKey: [ + "project", + props.project.id, + "server-wallets", + props.managementAccessToken, + ], + }); + router.refresh(); + }, + }); + + return ( +
+ + Change Project Wallet + + Choose a server wallet to use as project wallet + + + +
+ {props.serverWallets.isPending ? ( +
+ +
+ ) : ( +
+ +
+ )} +
+ +
+ + +
+
+ ); +} + +const createSendFormSchema = (secretKeyLabel: string) => + z.object({ + chainId: z.number({ + required_error: "Select a network", + }), + toAddress: z + .string() + .trim() + .min(1, "Destination address is required") + .refine((value) => Boolean(isAddress(value)), { + message: "Enter a valid wallet address", + }), + amount: z.string().trim().min(1, "Amount is required"), + secretKey: z.string().trim().min(1, `${secretKeyLabel} is required`), + vaultAccessToken: z.string().trim(), + }); + +const SECRET_KEY_LABEL = "Project secret key"; + +type SendFormValues = z.infer>; + +type SendProjectWalletModalProps = { + open: boolean; + onClose: () => void; + onSuccess: () => void; + walletAddress: string; + publishableKey: string; + teamId: string; + chainId: number; + label: string; + client: ReturnType; + isManagedVault: boolean; +}; + +const secretKeyLabel = SECRET_KEY_LABEL; +const secretKeyPlaceholder = "Enter your project secret key"; +const secretKeyHelper = + "Your project secret key was generated when you created your project. If you lost it, regenerate one from Project settings."; +const vaultAccessTokenHelper = + "Vault access tokens are optional credentials with server wallet permissions. Manage them in Vault settings."; + +function SendProjectWalletModal(props: SendProjectWalletModalProps) { + return ( + { + if (!nextOpen) { + props.onClose(); + } + }} + open={props.open} + > + + + + + ); +} + +function SendProjectWalletModalContent(props: SendProjectWalletModalProps) { + const { + onClose, + onSuccess, + walletAddress, + publishableKey, + teamId, + chainId, + label, + client, + isManagedVault, + } = props; + + const formSchema = useMemo(() => createSendFormSchema(SECRET_KEY_LABEL), []); + + const form = useForm({ + defaultValues: { + amount: "0", + chainId, + secretKey: "", + vaultAccessToken: "", + toAddress: "", + }, + mode: "onChange", + resolver: zodResolver(formSchema), + }); + + const selectedChain = useV5DashboardChain(form.watch("chainId")); + + const sendMutation = useMutation({ + mutationFn: async (values: SendFormValues) => { + const quantityWei = toWei(values.amount).toString(); + const secretKeyValue = values.secretKey.trim(); + const vaultAccessTokenValue = values.vaultAccessToken.trim(); + + const result = await sendProjectWalletTokens({ + chainId: values.chainId, + publishableKey, + quantityWei, + recipientAddress: values.toAddress, + teamId, + walletAddress, + secretKey: secretKeyValue, + ...(vaultAccessTokenValue + ? { vaultAccessToken: vaultAccessTokenValue } + : {}), + }); + + if (!result.ok) { + const errorMessage = + typeof result.error === "string" + ? result.error + : "Failed to send funds"; + throw new Error(errorMessage); + } + + return result.transactionIds; + }, + onError: (error) => { + toast.error(error.message); + }, + onSuccess: (transactionIds) => { + toast.success("Transfer submitted", { + description: + transactionIds && transactionIds.length > 0 + ? `Transaction ID: ${transactionIds[0]}` + : undefined, + }); + onSuccess(); + onClose(); + }, + }); + + return ( +
+ + Send from {label} + + Execute a one-off transfer using your server wallet. + + + +
+ { + void sendMutation.mutateAsync(values); + })} + > +
+ ( + + Network + + { + field.onChange(nextChainId); + }} + placeholder="Select network" + /> + + + + )} + /> + + ( + + Send to + + + + + + )} + /> + + ( + + Amount + + + + + Sending native token + {selectedChain?.nativeCurrency?.symbol + ? ` (${selectedChain.nativeCurrency.symbol})` + : ""} + + + + )} + /> + + ( + + {secretKeyLabel} + + + + {secretKeyHelper} + + + )} + /> + + {!isManagedVault && ( + ( + + + Vault access token + (optional) + + + + + {vaultAccessTokenHelper} + + + )} + /> + )} +
+ +
+ + +
+
+ +
+ ); +} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/project-wallet/project-wallet.stories.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/project-wallet/project-wallet.stories.tsx new file mode 100644 index 00000000000..750c51f3328 --- /dev/null +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/project-wallet/project-wallet.stories.tsx @@ -0,0 +1,126 @@ +import type { Meta, StoryObj } from "@storybook/nextjs"; +import type { ProjectWalletSummary } from "@/lib/server/project-wallet"; +import { projectStub } from "@/storybook/stubs"; +import { storybookThirdwebClient } from "@/storybook/utils"; +import { ProjectWalletSectionUI } from "./project-wallet"; + +const meta = { + component: ProjectWalletSectionUI, + title: "Project/ProjectWalletSection", + parameters: { + nextjs: { + appDirectory: true, + }, + }, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +const projectWithManagedAccessToken = projectStub("foo", "bar"); +projectWithManagedAccessToken.services = [ + { + name: "engineCloud", + actions: [], + managementAccessToken: "managed-access-token", + }, +]; + +const projectWithoutManagedAccessToken = projectStub("foo", "bar"); + +const projectWallet1: ProjectWalletSummary = { + id: "server-wallet-id", + address: "0x1F846F6DAE38E1C88D71EAA191760B15f38B7A37", + label: "Project Wallet 1", +}; + +const projectWallet2: ProjectWalletSummary = { + id: "server-wallet-id-2", + address: "0x83Dd93fA5D8343094f850f90B3fb90088C1bB425", + label: "Project Wallet 2", +}; + +export const NoProjectWalletSetNoManagedAccessToken: Story = { + args: { + project: projectWithoutManagedAccessToken, + client: storybookThirdwebClient, + teamSlug: "bar", + projectWallet: undefined, + getProjectServerWallets: async () => { + await new Promise((resolve) => setTimeout(resolve, 1000)); + return []; + }, + }, +}; + +export const NoProjectWalletSetWithManagedAccessToken: Story = { + args: { + project: projectWithManagedAccessToken, + client: storybookThirdwebClient, + teamSlug: "bar", + projectWallet: undefined, + getProjectServerWallets: async () => { + await new Promise((resolve) => setTimeout(resolve, 1000)); + return []; + }, + }, +}; + +export const NoProjectWalletSetWithManagedAccessTokenAndServerWallets: Story = { + args: { + project: projectWithManagedAccessToken, + teamSlug: "bar", + client: storybookThirdwebClient, + projectWallet: undefined, + getProjectServerWallets: async () => { + await new Promise((resolve) => setTimeout(resolve, 1000)); + return [projectWallet1, projectWallet2]; + }, + }, +}; + +export const NoProjectWalletSetLoading: Story = { + args: { + project: projectWithManagedAccessToken, + teamSlug: "bar", + client: storybookThirdwebClient, + projectWallet: undefined, + getProjectServerWallets: async () => { + await new Promise((resolve) => setTimeout(resolve, 100000)); + return [projectWallet1, projectWallet2]; + }, + }, +}; + +export const ProjectWalletSetMultipleServerWallets: Story = { + args: { + project: projectWithManagedAccessToken, + teamSlug: "bar", + client: storybookThirdwebClient, + projectWallet: projectWallet1, + getProjectServerWallets: async () => { + await new Promise((resolve) => setTimeout(resolve, 1000)); + return [projectWallet1, projectWallet2]; + }, + }, +}; + +export const ProjectWalletSetSingleServerWallet: Story = { + args: { + project: projectWithManagedAccessToken, + teamSlug: "bar", + projectWallet: projectWallet1, + client: storybookThirdwebClient, + getProjectServerWallets: async () => { + await new Promise((resolve) => setTimeout(resolve, 1000)); + return [projectWallet1]; + }, + }, +}; diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/project-wallet/project-wallet.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/project-wallet/project-wallet.tsx new file mode 100644 index 00000000000..e2a44ce2208 --- /dev/null +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/project-wallet/project-wallet.tsx @@ -0,0 +1,331 @@ +"use client"; + +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { UnderlineLink } from "@workspace/ui/components/UnderlineLink"; +import { ChevronDownIcon, XIcon } from "lucide-react"; +import { useMemo, useState } from "react"; +import { toast } from "sonner"; +import type { ThirdwebClient } from "thirdweb"; +import { listProjectServerWallets } from "@/actions/project-wallet/list-server-wallets"; +import type { Project } from "@/api/project/projects"; +import { WalletAddress } from "@/components/blocks/wallet-address"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Spinner } from "@/components/ui/Spinner"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Skeleton } from "@/components/ui/skeleton"; +import { getClientThirdwebClient } from "@/constants/thirdweb-client.client"; +import { useDashboardRouter } from "@/lib/DashboardRouter"; +import type { ProjectWalletSummary } from "@/lib/server/project-wallet"; +import { updateDefaultProjectWallet } from "../../transactions/lib/vault.client"; +import { CreateServerWallet } from "../../transactions/server-wallets/components/create-server-wallet.client"; +import { ProjectWalletDetailsSection } from "./project-wallet-details"; + +type GetProjectServerWallets = (params: { + managementAccessToken: string; + projectId: string; +}) => Promise; + +function CreateProjectWalletSection(props: { + project: Project; + teamSlug: string; + getProjectServerWallets: GetProjectServerWallets; +}) { + const { project, teamSlug } = props; + const router = useDashboardRouter(); + const queryClient = useQueryClient(); + const client = useMemo(() => getClientThirdwebClient(), []); + const [selectedWalletId, setSelectedWalletId] = useState( + undefined, + ); + const [isSelectDialogOpen, setIsSelectDialogOpen] = useState(false); + + const managementAccessToken = + props.project.services?.find((service) => service.name === "engineCloud") + ?.managementAccessToken ?? undefined; + + const serverWalletsQuery = useQuery({ + enabled: Boolean(managementAccessToken), + queryFn: async () => { + if (!managementAccessToken) { + return [] as ProjectWalletSummary[]; + } + + return props.getProjectServerWallets({ + managementAccessToken, + projectId: project.id, + }); + }, + queryKey: ["project", project.id, "server-wallets", managementAccessToken], + refetchOnWindowFocus: false, + }); + + const serverWallets = serverWalletsQuery.data ?? []; + + const effectiveSelectedWalletId = useMemo(() => { + if ( + selectedWalletId && + serverWallets.some((wallet) => wallet.id === selectedWalletId) + ) { + return selectedWalletId; + } + + return serverWallets[0]?.id; + }, [selectedWalletId, serverWallets]); + + const selectedWallet = useMemo(() => { + if (!effectiveSelectedWalletId) { + return undefined; + } + + return serverWallets.find( + (wallet) => wallet.id === effectiveSelectedWalletId, + ); + }, [effectiveSelectedWalletId, serverWallets]); + + const setDefaultWalletMutation = useMutation({ + mutationFn: async (wallet: ProjectWalletSummary) => { + await updateDefaultProjectWallet({ + project, + projectWalletAddress: wallet.address, + }); + }, + onError: (error) => { + const message = + error instanceof Error + ? error.message + : "Failed to update project wallet"; + toast.error(message); + }, + onSuccess: async (_, wallet) => { + const descriptionLabel = wallet.label ?? wallet.address; + toast.success("Project wallet updated", { + description: `Now pointing to ${descriptionLabel}`, + }); + await queryClient.invalidateQueries({ + queryKey: [ + "project", + project.id, + "server-wallets", + managementAccessToken, + ], + }); + setIsSelectDialogOpen(false); + router.refresh(); + }, + }); + + const canSelectExisting = + Boolean(managementAccessToken) && serverWallets.length > 0; + + return ( +
+
+ +
+

+ No Project Wallet set +

+

+ Set a project wallet to set the default sender in thirdweb API. +

+ +
+ {serverWalletsQuery.isLoading ? ( + + ) : canSelectExisting ? ( +
+ + + + + { + if (!nextOpen) { + setIsSelectDialogOpen(false); + } + }} + open={isSelectDialogOpen} + > + + + Select project wallet + + Choose a server wallet to use as the default for this + project. + + + +
+ {serverWalletsQuery.isLoading ? ( +
+ +
+ ) : serverWallets.length === 0 ? ( +

+ No server wallets found. +

+ ) : ( +
+ +
+ )} +
+ +
+ + +
+
+
+
+ ) : ( +
+ +
+ )} +
+
+ ); +} + +export function ProjectWalletSectionUI(props: { + project: Project; + teamSlug: string; + projectWallet: ProjectWalletSummary | undefined; + getProjectServerWallets: GetProjectServerWallets; + client: ThirdwebClient; +}) { + return ( +
+
+

+ Project Wallet +

+

+ The server wallet to be used as the default sender in{" "} + + thirdweb API + +

+
+ + {props.projectWallet ? ( + + ) : ( + + )} +
+ ); +} + +export function ProjectWalletSection(props: { + project: Project; + teamSlug: string; + projectWallet: ProjectWalletSummary | undefined; + client: ThirdwebClient; +}) { + return ( + + ); +} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/page.tsx index d3a4e3540c8..f2bdf920278 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/page.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/page.tsx @@ -39,10 +39,8 @@ import type { import { loginRedirect } from "@/utils/redirects"; import { PieChartCard } from "../../../components/Analytics/PieChartCard"; import { EngineCloudChartCardAsync } from "./components/EngineCloudChartCard"; -import { - ProjectFTUX, - ProjectWalletSection, -} from "./components/ProjectFTUX/ProjectFTUX"; +import { ProjectFTUX } from "./components/ProjectFTUX/ProjectFTUX"; +import { ProjectWalletSection } from "./components/project-wallet/project-wallet"; import { RpcMethodBarChartCardAsync } from "./components/RpcMethodBarChartCard"; import { TransactionsChartCardAsync } from "./components/Transactions"; import { ProjectHighlightsCard } from "./overview/highlights-card"; @@ -112,9 +110,6 @@ export default async function ProjectOverviewPage(props: PageProps) { }); const projectWallet = await getProjectWallet(project); - const managementAccessToken = - project.services?.find((service) => service.name === "engineCloud") - ?.managementAccessToken ?? undefined; return ( @@ -135,14 +130,17 @@ export default async function ProjectOverviewPage(props: PageProps) { actions: null, }} > + + +
+ {isActive ? (
-
) : ( - + )} 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 fdde607af18..83b7dde763f 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 @@ -3,14 +3,13 @@ import { useMemo } from "react"; import type { ThirdwebClient } from "thirdweb"; import type { Project } from "@/api/project/projects"; import { type Step, StepsCard } from "@/components/blocks/StepsCard"; -import CreateServerWallet from "../server-wallets/components/create-server-wallet.client"; +import { CreateServerWallet } from "../server-wallets/components/create-server-wallet.client"; import type { Wallet } from "../server-wallets/wallet-table/types"; import type { SolanaWallet } from "../solana-wallets/wallet-table/types"; import { SendTestSolanaTransaction } from "./send-test-solana-tx.client"; import { SendTestTransaction } from "./send-test-tx.client"; interface Props { - managementAccessToken: string | undefined; project: Project; wallets: Wallet[]; solanaWallets: SolanaWallet[]; @@ -29,11 +28,12 @@ export const EngineChecklist: React.FC = (props) => { const steps: Step[] = []; steps.push({ children: ( - +
+ +
), completed: props.wallets.length > 0 || props.hasTransactions, description: @@ -61,7 +61,6 @@ export const EngineChecklist: React.FC = (props) => { }); return steps; }, [ - props.managementAccessToken, props.project, props.wallets, props.hasTransactions, @@ -113,19 +112,3 @@ export const EngineChecklist: React.FC = (props) => { /> ); }; - -function CreateServerWalletStep(props: { - project: Project; - teamSlug: string; - managementAccessToken: string | undefined; -}) { - return ( -
- -
- ); -} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/components/server-wallets-table.client.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/components/server-wallets-table.client.tsx index c8b124715fe..bfcf5590f83 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/components/server-wallets-table.client.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/components/server-wallets-table.client.tsx @@ -58,7 +58,7 @@ import { WalletProductIcon } from "@/icons/WalletProductIcon"; import { useDashboardRouter } from "@/lib/DashboardRouter"; import { cn } from "@/lib/utils"; import { updateDefaultProjectWallet } from "../lib/vault.client"; -import CreateServerWallet from "../server-wallets/components/create-server-wallet.client"; +import { CreateServerWallet } from "../server-wallets/components/create-server-wallet.client"; import type { Wallet as EVMWallet } from "../server-wallets/wallet-table/types"; import { CreateSolanaWallet } from "../solana-wallets/components/create-solana-wallet.client"; import { UpgradeSolanaPermissions } from "../solana-wallets/components/upgrade-solana-permissions.client"; @@ -77,7 +77,6 @@ interface ServerWalletsTableProps { solanaTotalPages: number; project: Project; teamSlug: string; - managementAccessToken: string | undefined; client: ThirdwebClient; solanaPermissionError?: boolean; } @@ -88,7 +87,6 @@ export function ServerWalletsTable(props: ServerWalletsTableProps) { solanaWallets, project, teamSlug, - managementAccessToken, evmTotalRecords, evmCurrentPage, evmTotalPages, @@ -134,11 +132,7 @@ export function ServerWalletsTable(props: ServerWalletsTableProps) {
{activeChain === "evm" && ( <> - + service.name === "engineCloud") + ?.managementAccessToken ?? undefined; + const createEoaMutation = useMutation({ mutationFn: async ({ managementAccessToken, @@ -53,12 +61,12 @@ export default function CreateServerWallet(props: { }); const handleCreateServerWallet = async () => { - if (!props.managementAccessToken) { + if (!managementAccessToken) { router.push(`/team/${props.teamSlug}/${props.project.slug}/vault`); } else { await createEoaMutation.mutateAsync({ label, - managementAccessToken: props.managementAccessToken, + managementAccessToken: managementAccessToken, }); } }; @@ -68,21 +76,32 @@ export default function CreateServerWallet(props: { return ( <>