Skip to content

Commit 28a72ae

Browse files
committed
feat: Add wallet credentials support for Engine dashboard
1 parent 6ad7515 commit 28a72ae

File tree

15 files changed

+1077
-26
lines changed

15 files changed

+1077
-26
lines changed

apps/dashboard/src/@3rdweb-sdk/react/cache-keys.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,4 +125,6 @@ export const engineKeys = {
125125
[...engineKeys.all, engineId, "alerts"] as const,
126126
notificationChannels: (engineId: string) =>
127127
[...engineKeys.all, engineId, "notificationChannels"] as const,
128+
walletCredentials: (instance: string) =>
129+
[...engineKeys.all, instance, "walletCredentials"] as const,
128130
};

apps/dashboard/src/@3rdweb-sdk/react/hooks/useEngine.ts

Lines changed: 132 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,8 @@ type EngineFeature =
9393
| "CONTRACT_SUBSCRIPTIONS"
9494
| "IP_ALLOWLIST"
9595
| "HETEROGENEOUS_WALLET_TYPES"
96-
| "SMART_BACKEND_WALLETS";
96+
| "SMART_BACKEND_WALLETS"
97+
| "WALLET_CREDENTIALS";
9798

9899
interface EngineSystemHealth {
99100
status: string;
@@ -860,6 +861,10 @@ export type SetWalletConfigInput =
860861
gcpKmsKeyRingId: string;
861862
gcpApplicationCredentialEmail: string;
862863
gcpApplicationCredentialPrivateKey: string;
864+
}
865+
| {
866+
type: "circle";
867+
circleApiKey: string;
863868
};
864869

865870
export function useEngineSetWalletConfig(params: {
@@ -894,10 +899,17 @@ export function useEngineSetWalletConfig(params: {
894899
});
895900
}
896901

897-
export type CreateBackendWalletInput = {
898-
type: EngineBackendWalletType;
899-
label?: string;
900-
};
902+
export type CreateBackendWalletInput =
903+
| {
904+
type: Exclude<EngineBackendWalletType, "circle" | "smart:circle">;
905+
label?: string;
906+
}
907+
| {
908+
type: "circle" | "smart:circle";
909+
label?: string;
910+
credentialId: string;
911+
isTestnet: boolean;
912+
};
901913

902914
export function useEngineCreateBackendWallet(params: {
903915
instanceUrl: string;
@@ -1851,3 +1863,118 @@ export function useEngineDeleteNotificationChannel(engineId: string) {
18511863
},
18521864
});
18531865
}
1866+
1867+
export interface WalletCredential {
1868+
id: string;
1869+
type: string;
1870+
label: string;
1871+
isDefault: boolean | null;
1872+
createdAt: string;
1873+
updatedAt: string;
1874+
}
1875+
1876+
export interface CreateWalletCredentialInput {
1877+
type: "circle";
1878+
label: string;
1879+
entitySecret?: string;
1880+
isDefault?: boolean;
1881+
}
1882+
1883+
export function useEngineWalletCredentials(params: {
1884+
instanceUrl: string;
1885+
authToken: string;
1886+
page?: number;
1887+
limit?: number;
1888+
}) {
1889+
const { instanceUrl, authToken, page = 1, limit = 100 } = params;
1890+
1891+
return useQuery({
1892+
queryKey: [...engineKeys.walletCredentials(instanceUrl), page, limit],
1893+
queryFn: async () => {
1894+
const res = await fetch(
1895+
`${instanceUrl}wallet-credentials?page=${page}&limit=${limit}`,
1896+
{
1897+
method: "GET",
1898+
headers: getEngineRequestHeaders(authToken),
1899+
},
1900+
);
1901+
1902+
const json = await res.json();
1903+
return (json.result as WalletCredential[]) || [];
1904+
},
1905+
enabled: !!instanceUrl,
1906+
});
1907+
}
1908+
1909+
export function useEngineCreateWalletCredential(params: {
1910+
instanceUrl: string;
1911+
authToken: string;
1912+
}) {
1913+
const { instanceUrl, authToken } = params;
1914+
const queryClient = useQueryClient();
1915+
1916+
return useMutation({
1917+
mutationFn: async (input: CreateWalletCredentialInput) => {
1918+
invariant(instanceUrl, "instance is required");
1919+
1920+
const res = await fetch(`${instanceUrl}wallet-credentials`, {
1921+
method: "POST",
1922+
headers: getEngineRequestHeaders(authToken),
1923+
body: JSON.stringify(input),
1924+
});
1925+
const json = await res.json();
1926+
1927+
if (json.error) {
1928+
throw new Error(json.error.message);
1929+
}
1930+
1931+
return json.result as WalletCredential;
1932+
},
1933+
onSuccess: () => {
1934+
return queryClient.invalidateQueries({
1935+
queryKey: engineKeys.walletCredentials(instanceUrl),
1936+
});
1937+
},
1938+
});
1939+
}
1940+
1941+
export interface UpdateWalletCredentialInput {
1942+
label?: string;
1943+
isDefault?: boolean;
1944+
entitySecret?: string;
1945+
}
1946+
1947+
export function useEngineUpdateWalletCredential(params: {
1948+
instanceUrl: string;
1949+
authToken: string;
1950+
}) {
1951+
const { instanceUrl, authToken } = params;
1952+
const queryClient = useQueryClient();
1953+
1954+
return useMutation({
1955+
mutationFn: async ({
1956+
id,
1957+
...input
1958+
}: UpdateWalletCredentialInput & { id: string }) => {
1959+
invariant(instanceUrl, "instance is required");
1960+
1961+
const res = await fetch(`${instanceUrl}wallet-credentials/${id}`, {
1962+
method: "PUT",
1963+
headers: getEngineRequestHeaders(authToken),
1964+
body: JSON.stringify(input),
1965+
});
1966+
const json = await res.json();
1967+
1968+
if (json.error) {
1969+
throw new Error(json.error.message);
1970+
}
1971+
1972+
return json.result as WalletCredential;
1973+
},
1974+
onSuccess: () => {
1975+
return queryClient.invalidateQueries({
1976+
queryKey: engineKeys.walletCredentials(instanceUrl),
1977+
});
1978+
},
1979+
});
1980+
}

apps/dashboard/src/app/team/[team_slug]/(team)/~/engine/(instance)/[engineId]/_components/EnginePageLayout.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ const sidebarLinkMeta: Array<{ pathId: string; label: string }> = [
3838
pathId: "alerts",
3939
label: "Alerts",
4040
},
41+
{
42+
pathId: "wallet-credentials",
43+
label: "Wallet Credentials",
44+
},
4145
{
4246
pathId: "configuration",
4347
label: "Configuration",
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import { FormFieldSetup } from "@/components/blocks/FormFieldSetup";
2+
import { Spinner } from "@/components/ui/Spinner/Spinner";
3+
import { Button } from "@/components/ui/button";
4+
import { Form } from "@/components/ui/form";
5+
import { Input } from "@/components/ui/input";
6+
import { TrackedLinkTW } from "@/components/ui/tracked-link";
7+
import {
8+
type EngineInstance,
9+
type SetWalletConfigInput,
10+
useEngineSetWalletConfig,
11+
} from "@3rdweb-sdk/react/hooks/useEngine";
12+
import { useTrack } from "hooks/analytics/useTrack";
13+
import { useTxNotifications } from "hooks/useTxNotifications";
14+
import { useForm } from "react-hook-form";
15+
import { Text } from "tw-components";
16+
17+
interface CircleConfigProps {
18+
instance: EngineInstance;
19+
authToken: string;
20+
}
21+
22+
export const CircleConfig: React.FC<CircleConfigProps> = ({
23+
instance,
24+
authToken,
25+
}) => {
26+
const { mutate: setCircleConfig, isPending } = useEngineSetWalletConfig({
27+
instanceUrl: instance.url,
28+
authToken,
29+
});
30+
const trackEvent = useTrack();
31+
const { onSuccess, onError } = useTxNotifications(
32+
"Configuration set successfully.",
33+
"Failed to set configuration.",
34+
);
35+
36+
const defaultValues: SetWalletConfigInput = {
37+
type: "circle" as const,
38+
circleApiKey: "",
39+
};
40+
41+
const form = useForm<SetWalletConfigInput>({
42+
defaultValues,
43+
values: defaultValues,
44+
resetOptions: {
45+
keepDirty: true,
46+
keepDirtyValues: true,
47+
},
48+
});
49+
50+
const onSubmit = (data: SetWalletConfigInput) => {
51+
setCircleConfig(data, {
52+
onSuccess: () => {
53+
onSuccess();
54+
trackEvent({
55+
category: "engine",
56+
action: "set-wallet-config",
57+
type: "circle",
58+
label: "success",
59+
});
60+
},
61+
onError: (error) => {
62+
onError(error);
63+
trackEvent({
64+
category: "engine",
65+
action: "set-wallet-config",
66+
type: "circle",
67+
label: "error",
68+
error,
69+
});
70+
},
71+
});
72+
};
73+
74+
return (
75+
<div className="flex flex-col gap-6">
76+
<div className="flex flex-col gap-2">
77+
<Text className="text-muted-foreground">
78+
Circle wallets require an API Key from your Circle account with
79+
sufficient permissions. Created wallets are stored in your AWS
80+
account. Configure your Circle API Key to use Circle wallets. Learn
81+
more about{" "}
82+
<TrackedLinkTW
83+
href="https://portal.thirdweb.com/engine/features/backend-wallets#circle-wallet"
84+
target="_blank"
85+
label="learn-more"
86+
category="engine"
87+
className="text-link-foreground hover:text-foreground"
88+
>
89+
how to get an API Key
90+
</TrackedLinkTW>
91+
.
92+
</Text>
93+
</div>
94+
95+
<Form {...form}>
96+
<form
97+
className="flex flex-col gap-4"
98+
onSubmit={form.handleSubmit(onSubmit)}
99+
>
100+
<FormFieldSetup
101+
label="Circle API Key"
102+
errorMessage={
103+
form.getFieldState("circleApiKey", form.formState).error?.message
104+
}
105+
htmlFor="circle-api-key"
106+
isRequired
107+
tooltip={null}
108+
>
109+
<Input
110+
id="circle-api-key"
111+
placeholder="TEST_API_KEY:..."
112+
autoComplete="off"
113+
type="password"
114+
{...form.register("circleApiKey")}
115+
/>
116+
</FormFieldSetup>
117+
118+
<div className="flex items-center justify-end gap-4">
119+
<Button
120+
type="submit"
121+
className="min-w-28 gap-2"
122+
disabled={isPending}
123+
>
124+
{isPending && <Spinner className="size-4" />}
125+
Save
126+
</Button>
127+
</div>
128+
</form>
129+
</Form>
130+
</div>
131+
);
132+
};

apps/dashboard/src/app/team/[team_slug]/(team)/~/engine/(instance)/[engineId]/configuration/components/engine-wallet-config.tsx

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
12
import { TabButtons } from "@/components/ui/tabs";
23
import { ToolTipLabel } from "@/components/ui/tooltip";
34
import { cn } from "@/lib/utils";
45
import {
56
type EngineInstance,
67
useEngineWalletConfig,
8+
useHasEngineFeature,
79
} from "@3rdweb-sdk/react/hooks/useEngine";
810
import {
911
EngineBackendWalletOptions,
@@ -13,6 +15,7 @@ import { CircleAlertIcon } from "lucide-react";
1315
import Link from "next/link";
1416
import { useState } from "react";
1517
import { Heading } from "tw-components";
18+
import { CircleConfig } from "./circle-config";
1619
import { KmsAwsConfig } from "./kms-aws-config";
1720
import { KmsGcpConfig } from "./kms-gcp-config";
1821
import { LocalConfig } from "./local-config";
@@ -33,11 +36,28 @@ export const EngineWalletConfig: React.FC<EngineWalletConfigProps> = ({
3336
authToken,
3437
});
3538

39+
const { isSupported: isWalletCredentialsSupported } = useHasEngineFeature(
40+
instance.url,
41+
"WALLET_CREDENTIALS",
42+
);
43+
44+
const filteredWalletOptions = EngineBackendWalletOptions.filter(
45+
({ key }) =>
46+
// circle wallets were only added with the WALLET_CREDENTIALS feature flag
47+
(key !== "circle" || isWalletCredentialsSupported) &&
48+
// smart wallets don't need separate configuration
49+
!key.startsWith("smart:"),
50+
);
51+
3652
const tabContent: Partial<Record<EngineBackendWalletType, React.ReactNode>> =
3753
{
3854
local: <LocalConfig />,
3955
"aws-kms": <KmsAwsConfig instance={instance} authToken={authToken} />,
4056
"gcp-kms": <KmsGcpConfig instance={instance} authToken={authToken} />,
57+
// circle wallets were only added with the WALLET_CREDENTIALS feature flag
58+
...(isWalletCredentialsSupported && {
59+
circle: <CircleConfig instance={instance} authToken={authToken} />,
60+
}),
4161
} as const;
4262

4363
const [activeTab, setActiveTab] = useState<EngineBackendWalletType>("local");
@@ -62,7 +82,7 @@ export const EngineWalletConfig: React.FC<EngineWalletConfigProps> = ({
6282
</div>
6383

6484
<TabButtons
65-
tabs={EngineBackendWalletOptions.map(({ key, name }) => ({
85+
tabs={filteredWalletOptions.map(({ key, name }) => ({
6686
key,
6787
name,
6888
isActive: activeTab === key,
@@ -81,6 +101,17 @@ export const EngineWalletConfig: React.FC<EngineWalletConfigProps> = ({
81101
tabClassName="font-medium !text-sm"
82102
/>
83103

104+
{!isWalletCredentialsSupported && activeTab === "circle" && (
105+
<Alert variant="warning" className="mt-4">
106+
<CircleAlertIcon className="size-4" />
107+
<AlertTitle>Update Required</AlertTitle>
108+
<AlertDescription>
109+
Circle wallet support requires a newer version of Engine. Please
110+
update your Engine instance to use this feature.
111+
</AlertDescription>
112+
</Alert>
113+
)}
114+
84115
{tabContent[activeTab]}
85116
</div>
86117
);

0 commit comments

Comments
 (0)