diff --git a/apps/dashboard/src/@3rdweb-sdk/react/hooks/useEngine.ts b/apps/dashboard/src/@3rdweb-sdk/react/hooks/useEngine.ts index 549e3772aba..7b8b32d7f25 100644 --- a/apps/dashboard/src/@3rdweb-sdk/react/hooks/useEngine.ts +++ b/apps/dashboard/src/@3rdweb-sdk/react/hooks/useEngine.ts @@ -3,6 +3,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import type { ResultItem } from "components/engine/system-metrics/components/StatusCodes"; import { THIRDWEB_API_HOST } from "constants/urls"; +import type { EngineBackendWalletType } from "lib/engine"; import { useState } from "react"; import { useActiveAccount, useActiveWalletChain } from "thirdweb/react"; import invariant from "tiny-invariant"; @@ -111,10 +112,16 @@ export function useEngineBackendWallets(instance: string) { }); } +type EngineFeature = + | "KEYPAIR_AUTH" + | "CONTRACT_SUBSCRIPTIONS" + | "IP_ALLOWLIST" + | "HETEROGENEOUS_WALLET_TYPES"; + interface EngineSystemHealth { status: string; engineVersion?: string; - features?: string[]; + features?: EngineFeature[]; } export function useEngineSystemHealth( @@ -138,6 +145,18 @@ export function useEngineSystemHealth( }); } +// Helper function to check if a feature is supported. +export function useHasEngineFeature( + instanceUrl: string, + feature: EngineFeature, +) { + const query = useEngineSystemHealth(instanceUrl); + return { + query, + isSupported: !!query.data?.features?.includes(feature), + }; +} + interface EngineSystemQueueMetrics { result: { queued: number; @@ -188,17 +207,15 @@ export function useEngineLatestVersion() { } interface UpdateVersionInput { - engineId: string; + deploymentId: string; serverVersion: string; } export function useEngineUpdateServerVersion() { return useMutation({ mutationFn: async (input: UpdateVersionInput) => { - invariant(input.engineId, "engineId is required"); - const res = await fetch( - `${THIRDWEB_API_HOST}/v2/engine/${input.engineId}/infrastructure`, + `${THIRDWEB_API_HOST}/v2/engine/deployments/${input.deploymentId}/infrastructure`, { method: "PUT", headers: { @@ -401,29 +418,22 @@ export function useEngineTransactions(instance: string, autoUpdate: boolean) { }); } -type WalletConfig = - | { - type: "local"; - } - | { - type: "aws-kms"; - awsAccessKeyId: string; - awsSecretAccessKey: string; - awsRegion: string; - } - | { - type: "gcp-kms"; - gcpApplicationProjectId: string; - gcpKmsLocationId: string; - gcpKmsKeyRingId: string; - gcpApplicationCredentialEmail: string; - gcpApplicationCredentialPrivateKey: string; - }; +export interface WalletConfigResponse { + type: EngineBackendWalletType; + + awsAccessKeyId?: string | null; + awsRegion?: string | null; + + gcpApplicationProjectId?: string | null; + gcpKmsLocationId?: string | null; + gcpKmsKeyRingId?: string | null; + gcpApplicationCredentialEmail?: string | null; +} export function useEngineWalletConfig(instance: string) { const token = useLoggedInUser().user?.jwt ?? null; - return useQuery({ + return useQuery({ queryKey: engineKeys.walletConfig(instance), queryFn: async () => { const res = await fetch(`${instance}configuration/wallets`, { @@ -432,8 +442,7 @@ export function useEngineWalletConfig(instance: string) { }); const json = await res.json(); - - return (json.result as WalletConfig) || {}; + return json.result; }, enabled: !!instance && !!token, }); @@ -799,9 +808,6 @@ export function useEngineWebhooks(instance: string) { // POST REQUESTS export type SetWalletConfigInput = - | { - type: "local"; - } | { type: "aws-kms"; awsAccessKeyId: string; @@ -821,8 +827,8 @@ export function useEngineSetWalletConfig(instance: string) { const token = useLoggedInUser().user?.jwt ?? null; const queryClient = useQueryClient(); - return useMutation({ - mutationFn: async (input: SetWalletConfigInput) => { + return useMutation({ + mutationFn: async (input) => { invariant(instance, "instance is required"); const res = await fetch(`${instance}configuration/wallets`, { @@ -847,6 +853,7 @@ export function useEngineSetWalletConfig(instance: string) { } export type CreateBackendWalletInput = { + type: EngineBackendWalletType; label?: string; }; @@ -913,25 +920,20 @@ export function useEngineUpdateBackendWallet(instance: string) { }); } -export type ImportBackendWalletInput = - | { - awsKmsKeyId: string; - awsKmsArn: string; - } - | { - gcpKmsKeyId: string; - gcpKmsKeyVersionId: string; - } - | { - privateKey?: string; - } - | { - mnemonic?: string; - } - | { - encryptedJson?: string; - password?: string; - }; +// The backend determines the wallet imported based on the provided fields. +export type ImportBackendWalletInput = { + label?: string; + + awsKmsArn?: string; + + gcpKmsKeyId?: string; + gcpKmsKeyVersionId?: string; + + privateKey?: string; + mnemonic?: string; + encryptedJson?: string; + password?: string; +}; export function useEngineImportBackendWallet(instance: string) { const token = useLoggedInUser().user?.jwt ?? null; @@ -1639,6 +1641,9 @@ export function useEngineCreateNotificationChannel(engineId: string) { `${THIRDWEB_API_HOST}/v1/engine/${engineId}/notification-channels`, { method: "POST", + headers: { + "Content-Type": "application/json", + }, body: JSON.stringify(input), }, ); @@ -1667,7 +1672,9 @@ export function useEngineDeleteNotificationChannel(engineId: string) { const res = await fetch( `${THIRDWEB_API_HOST}/v1/engine/${engineId}/notification-channels/${notificationChannelId}`, - { method: "DELETE" }, + { + method: "DELETE", + }, ); if (!res.ok) { throw new Error(`Unexpected status ${res.status}: ${await res.text()}`); diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/engine/(instance)/[engineId]/page.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/engine/(instance)/[engineId]/page.tsx index 21aa995388d..85c78a6421b 100644 --- a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/engine/(instance)/[engineId]/page.tsx +++ b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/engine/(instance)/[engineId]/page.tsx @@ -8,7 +8,7 @@ export default function Page(props: EngineInstancePageProps) { return ( } + content={(res) => } rootPath={`/team/${props.params.team_slug}/${props.params.project_slug}/engine`} /> ); diff --git a/apps/dashboard/src/components/engine/badges/version.tsx b/apps/dashboard/src/components/engine/badges/version.tsx index 8a83dd86641..390584f8623 100644 --- a/apps/dashboard/src/components/engine/badges/version.tsx +++ b/apps/dashboard/src/components/engine/badges/version.tsx @@ -20,6 +20,7 @@ import { } from "@/components/ui/dialog"; import { TrackedLinkTW } from "@/components/ui/tracked-link"; import { toast } from "sonner"; +import invariant from "tiny-invariant"; export const EngineVersionBadge = ({ instance, @@ -119,9 +120,11 @@ const UpdateVersionModal = (props: { } const onClick = async () => { + invariant(instance.deploymentId, "Engine is missing deploymentId."); + try { const promise = updateEngineServerMutation.mutateAsync({ - engineId: instance.id, + deploymentId: instance.deploymentId, serverVersion: latestVersion, }); toast.promise(promise, { diff --git a/apps/dashboard/src/components/engine/configuration/engine-configuration.tsx b/apps/dashboard/src/components/engine/configuration/engine-configuration.tsx index e461719359a..8c63c7feab1 100644 --- a/apps/dashboard/src/components/engine/configuration/engine-configuration.tsx +++ b/apps/dashboard/src/components/engine/configuration/engine-configuration.tsx @@ -15,7 +15,7 @@ export const EngineConfiguration: React.FC = ({ }) => { return (
- + diff --git a/apps/dashboard/src/components/engine/configuration/engine-wallet-config.tsx b/apps/dashboard/src/components/engine/configuration/engine-wallet-config.tsx index 5d37b96fd52..7d21fde3fb1 100644 --- a/apps/dashboard/src/components/engine/configuration/engine-wallet-config.tsx +++ b/apps/dashboard/src/components/engine/configuration/engine-wallet-config.tsx @@ -1,102 +1,81 @@ -import { useEngineWalletConfig } from "@3rdweb-sdk/react/hooks/useEngine"; -import { ButtonGroup, Flex, Icon } from "@chakra-ui/react"; +import { TabButtons } from "@/components/ui/tabs"; +import { ToolTipLabel } from "@/components/ui/tooltip"; +import { cn } from "@/lib/utils"; +import { + type EngineInstance, + useEngineWalletConfig, +} from "@3rdweb-sdk/react/hooks/useEngine"; +import { Flex } from "@chakra-ui/react"; +import { + EngineBackendWalletOptions, + type EngineBackendWalletType, +} from "lib/engine"; +import { CircleAlertIcon } from "lucide-react"; +import Link from "next/link"; import { useState } from "react"; -import { MdRadioButtonChecked, MdRadioButtonUnchecked } from "react-icons/md"; -import { Button, Heading, Link, Text } from "tw-components"; +import {} from "react-icons/md"; +import { Heading } from "tw-components"; import { KmsAwsConfig } from "./kms-aws-config"; import { KmsGcpConfig } from "./kms-gcp-config"; -import { LocalConfig } from "./local-config.tsx"; +import { LocalConfig } from "./local-config"; interface EngineWalletConfigProps { - instanceUrl: string; + instance: EngineInstance; } export const EngineWalletConfig: React.FC = ({ - instanceUrl, + instance, }) => { - const { data: localConfig } = useEngineWalletConfig(instanceUrl); - const [selected, setSelected] = useState<"aws-kms" | "gcp-kms" | "local">( - localConfig?.type || "local", - ); + const { data: walletConfig } = useEngineWalletConfig(instance.url); + + const tabContent: Record = { + local: , + "aws-kms": , + "gcp-kms": , + } as const; + + const [activeTab, setActiveTab] = useState("local"); + + const isAwsKmsConfigured = !!walletConfig?.awsAccessKeyId; + const isGcpKmsConfigured = !!walletConfig?.gcpKmsKeyRingId; return ( Backend Wallets - - Select the type of backend wallets to use.{" "} +

+ Create backend wallets on the{" "} - Learn more about backend wallets - - . - + Overview + {" "} + tab. To use other wallet types, configure them below. +

- - - - - - - {selected === "local" && } - {selected === "aws-kms" && } - {selected === "gcp-kms" && } - + ({ + key, + name, + isActive: activeTab === key, + isEnabled: true, + onClick: () => setActiveTab(key), + icon: + (key === "aws-kms" && !isAwsKmsConfigured) || + (key === "gcp-kms" && !isGcpKmsConfigured) + ? ({ className }) => ( + + + + ) + : undefined, + }))} + tabClassName="font-medium !text-sm" + /> + + {tabContent[activeTab]}
); }; diff --git a/apps/dashboard/src/components/engine/configuration/ip-allowlist.tsx b/apps/dashboard/src/components/engine/configuration/ip-allowlist.tsx index 6d27612a06b..dfc3a02eec5 100644 --- a/apps/dashboard/src/components/engine/configuration/ip-allowlist.tsx +++ b/apps/dashboard/src/components/engine/configuration/ip-allowlist.tsx @@ -2,7 +2,7 @@ import { InlineCode } from "@/components/ui/inline-code"; import { useEngineIpAllowlistConfiguration, useEngineSetIpAllowlistConfiguration, - useEngineSystemHealth, + useHasEngineFeature, } from "@3rdweb-sdk/react/hooks/useEngine"; import { Flex, Textarea } from "@chakra-ui/react"; import { useTxNotifications } from "hooks/useTxNotifications"; @@ -30,8 +30,7 @@ export const EngineIpAllowlistConfig: React.FC< "IP Allowlist updated successfully.", "Failed to update IP Allowlist", ); - - const { data: engineHealthInfo } = useEngineSystemHealth(instanceUrl); + const { isSupported } = useHasEngineFeature(instanceUrl, "IP_ALLOWLIST"); const form = useForm({ values: { raw: existingIpAllowlist?.join("\n") ?? "" }, @@ -61,7 +60,7 @@ export const EngineIpAllowlistConfig: React.FC< } }; - if (!engineHealthInfo?.features?.includes("IP_ALLOWLIST")) { + if (!isSupported) { return null; } diff --git a/apps/dashboard/src/components/engine/configuration/kms-aws-config.tsx b/apps/dashboard/src/components/engine/configuration/kms-aws-config.tsx index 495e662e007..0c951b13846 100644 --- a/apps/dashboard/src/components/engine/configuration/kms-aws-config.tsx +++ b/apps/dashboard/src/components/engine/configuration/kms-aws-config.tsx @@ -1,126 +1,177 @@ +import { FormFieldSetup } from "@/components/blocks/FormFieldSetup"; +import { Spinner } from "@/components/ui/Spinner/Spinner"; +import { Button } from "@/components/ui/button"; +import { Form, FormDescription } from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { TrackedLinkTW } from "@/components/ui/tracked-link"; import { + type EngineInstance, type SetWalletConfigInput, useEngineSetWalletConfig, useEngineWalletConfig, + useHasEngineFeature, } from "@3rdweb-sdk/react/hooks/useEngine"; -import { Flex, FormControl, Input } from "@chakra-ui/react"; import { useTrack } from "hooks/analytics/useTrack"; import { useTxNotifications } from "hooks/useTxNotifications"; import { useForm } from "react-hook-form"; -import { Button, Card, FormLabel, Text } from "tw-components"; interface KmsAwsConfigProps { - instanceUrl: string; + instance: EngineInstance; } -export const KmsAwsConfig: React.FC = ({ instanceUrl }) => { - const { mutate: setAwsKmsConfig } = useEngineSetWalletConfig(instanceUrl); - const { data: awsConfig } = useEngineWalletConfig(instanceUrl); +export const KmsAwsConfig: React.FC = ({ instance }) => { + const { mutate: setAwsKmsConfig, isPending } = useEngineSetWalletConfig( + instance.url, + ); + const { data: awsConfig } = useEngineWalletConfig(instance.url); + const { isSupported: supportsMultipleWalletTypes } = useHasEngineFeature( + instance.url, + "HETEROGENEOUS_WALLET_TYPES", + ); const trackEvent = useTrack(); const { onSuccess, onError } = useTxNotifications( "Configuration set successfully.", "Failed to set configuration.", ); - const transformedQueryData: SetWalletConfigInput = { + const defaultValues: SetWalletConfigInput = { type: "aws-kms" as const, - awsAccessKeyId: - awsConfig?.type === "aws-kms" ? (awsConfig?.awsAccessKeyId ?? "") : "", + awsAccessKeyId: awsConfig?.awsAccessKeyId ?? "", awsSecretAccessKey: "", - awsRegion: - awsConfig?.type === "aws-kms" ? (awsConfig?.awsRegion ?? "") : "", + awsRegion: awsConfig?.awsRegion ?? "", }; const form = useForm({ - defaultValues: transformedQueryData, - values: transformedQueryData, + defaultValues, + values: defaultValues, resetOptions: { keepDirty: true, keepDirtyValues: true, }, }); - return ( - { - setAwsKmsConfig(data, { - onSuccess: () => { - onSuccess(); - trackEvent({ - category: "engine", - action: "set-wallet-config", - type: "aws-kms", - label: "success", - }); - }, - onError: (error) => { - onError(error); - trackEvent({ - category: "engine", - action: "set-wallet-config", - type: "aws-kms", - label: "error", - error, - }); - }, + const onSubmit = (data: SetWalletConfigInput) => { + setAwsKmsConfig(data, { + onSuccess: () => { + onSuccess(); + trackEvent({ + category: "engine", + action: "set-wallet-config", + type: "aws-kms", + label: "success", + }); + }, + onError: (error) => { + onError(error); + trackEvent({ + category: "engine", + action: "set-wallet-config", + type: "aws-kms", + label: "error", + error, }); - })} - > - - - Engine supports AWS KWS for signing & sending transactions over any - EVM chain. - - - - Access Key + }, + }); + }; + + return ( +
+ +

+ AWS KMS wallets require credentials from your Amazon Web Services + account with sufficient permissions to manage KMS keys. Wallets are + stored in KMS keys on your AWS account. +

+

+ For help and more advanced use cases,{" "} + + learn more about using AWS KMS wallets + + . +

+ +
+ - - - Secret Key + + + - - - - Region - - - - - {form.formState.isDirty && ( - - This will reset your other backend wallet configurations - - )} - - - + + + + + + This will not be shown again. + + +
+ +
+ {!supportsMultipleWalletTypes && ( +

+ This will clear other credentials. +

+ )} + +
+
+ ); }; diff --git a/apps/dashboard/src/components/engine/configuration/kms-gcp-config.tsx b/apps/dashboard/src/components/engine/configuration/kms-gcp-config.tsx index 7af04965f10..a5f9c5b9508 100644 --- a/apps/dashboard/src/components/engine/configuration/kms-gcp-config.tsx +++ b/apps/dashboard/src/components/engine/configuration/kms-gcp-config.tsx @@ -1,165 +1,224 @@ +import { FormFieldSetup } from "@/components/blocks/FormFieldSetup"; +import { Spinner } from "@/components/ui/Spinner/Spinner"; +import { Button } from "@/components/ui/button"; +import { Form, FormDescription } from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { Textarea } from "@/components/ui/textarea"; +import { TrackedLinkTW } from "@/components/ui/tracked-link"; import { + type EngineInstance, type SetWalletConfigInput, useEngineSetWalletConfig, useEngineWalletConfig, + useHasEngineFeature, } from "@3rdweb-sdk/react/hooks/useEngine"; -import { Flex, FormControl, Input } from "@chakra-ui/react"; import { useTrack } from "hooks/analytics/useTrack"; import { useTxNotifications } from "hooks/useTxNotifications"; import { useForm } from "react-hook-form"; -import { Button, Card, FormLabel, Text } from "tw-components"; interface KmsGcpConfigProps { - instanceUrl: string; + instance: EngineInstance; } -export const KmsGcpConfig: React.FC = ({ instanceUrl }) => { - const { mutate: setGcpKmsConfig } = useEngineSetWalletConfig(instanceUrl); - const { data: gcpConfig } = useEngineWalletConfig(instanceUrl); +export const KmsGcpConfig: React.FC = ({ instance }) => { + const { mutate: setGcpKmsConfig, isPending } = useEngineSetWalletConfig( + instance.url, + ); + const { data: gcpConfig } = useEngineWalletConfig(instance.url); + const { isSupported: supportsMultipleWalletTypes } = useHasEngineFeature( + instance.url, + "HETEROGENEOUS_WALLET_TYPES", + ); const trackEvent = useTrack(); const { onSuccess, onError } = useTxNotifications( "Configuration set successfully.", "Failed to set configuration.", ); - const transformedQueryData: SetWalletConfigInput = { + const defaultValues: SetWalletConfigInput = { type: "gcp-kms" as const, - gcpApplicationProjectId: - gcpConfig?.type === "gcp-kms" - ? (gcpConfig?.gcpApplicationProjectId ?? "") - : "", - gcpKmsLocationId: - gcpConfig?.type === "gcp-kms" ? (gcpConfig?.gcpKmsLocationId ?? "") : "", - gcpKmsKeyRingId: - gcpConfig?.type === "gcp-kms" ? (gcpConfig?.gcpKmsKeyRingId ?? "") : "", + gcpApplicationProjectId: gcpConfig?.gcpApplicationProjectId ?? "", + gcpKmsLocationId: gcpConfig?.gcpKmsLocationId ?? "", + gcpKmsKeyRingId: gcpConfig?.gcpKmsKeyRingId ?? "", gcpApplicationCredentialEmail: - gcpConfig?.type === "gcp-kms" - ? (gcpConfig?.gcpApplicationCredentialEmail ?? "") - : "", - gcpApplicationCredentialPrivateKey: - gcpConfig?.type === "gcp-kms" - ? (gcpConfig?.gcpApplicationCredentialPrivateKey ?? "") - : "", + gcpConfig?.gcpApplicationCredentialEmail ?? "", + gcpApplicationCredentialPrivateKey: "", }; const form = useForm({ - defaultValues: transformedQueryData, - values: transformedQueryData, + defaultValues, + values: defaultValues, resetOptions: { keepDirty: true, keepDirtyValues: true, }, }); - return ( - { - setGcpKmsConfig(data, { - onSuccess: () => { - onSuccess(); - trackEvent({ - category: "engine", - action: "set-wallet-config", - type: "gcp-kms", - label: "success", - }); - }, - onError: (error) => { - onError(error); - trackEvent({ - category: "engine", - action: "set-wallet-config", - type: "gcp-kms", - label: "error", - error, - }); - }, + const onSubmit = (data: SetWalletConfigInput) => { + setGcpKmsConfig(data, { + onSuccess: () => { + onSuccess(); + trackEvent({ + category: "engine", + action: "set-wallet-config", + type: "gcp-kms", + label: "success", }); - })} - > - - - Engine supports Google KMS for signing & sending transactions over any - EVM chain. - - - - Location ID + }, + onError: (error) => { + onError(error); + trackEvent({ + category: "engine", + action: "set-wallet-config", + type: "gcp-kms", + label: "error", + error, + }); + }, + }); + }; + + return ( +
+ +

+ GCP KMS wallets require credentials from your Google Cloud Platform + account with sufficient permissions to manage KMS keys. Wallets are + stored in KMS keys on your GCP account. +

+

+ For help and more advanced use cases,{" "} + + learn more about using Google Cloud KMS wallets + + . +

+ +
+ - - - Key Ring ID + + + - - - - - Project ID + + + - - - Credential Email + + + - - - - Private Key - +
+ + +