From 36ab1f9716eeba0cf091d1926b596cbb23a22c1e Mon Sep 17 00:00:00 2001
From: 0xFirekeeper <0xFirekeeper@gmail.com>
Date: Thu, 27 Aug 2026 09:01:05 +0700
Subject: [PATCH] [Dashboard] Create vault service accounts through api-server
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Service-account creation was the last vault operation still run from the
browser. It now goes through the typed action layer to
POST /v1/teams/:team/projects/:project/vault/service-account, which mints the
management and wallet tokens and creates the default project wallet
server-side.
A managed vault no longer hands back the admin key or wallet token — the
server seals both with the project secret key — so the creation dialog shows
the masked admin key for managed vaults and keeps the download-and-confirm
flow for ejected ones.
upgradeAccessTokensForSolana still runs in the browser: no api-server endpoint
reissues a project's management and wallet tokens in place, so initVaultClient
stays for that one caller and is no longer exported.
Co-Authored-By: Claude Fable 5
---
apps/dashboard/src/@/actions/vault.ts | 37 +++
.../project/create-project-modal/index.tsx | 16 +-
.../_components/create-project-form.tsx | 19 +-
.../transactions/lib/vault.client.ts | 254 +-----------------
.../create-vault-account.client.tsx | 73 +++--
.../wallets/vault-recovery-card.client.tsx | 38 +--
6 files changed, 128 insertions(+), 309 deletions(-)
diff --git a/apps/dashboard/src/@/actions/vault.ts b/apps/dashboard/src/@/actions/vault.ts
index aa2bbe84c33..76421f45c9b 100644
--- a/apps/dashboard/src/@/actions/vault.ts
+++ b/apps/dashboard/src/@/actions/vault.ts
@@ -45,6 +45,11 @@ type RotateVaultServiceAccountResult = {
walletAccessToken?: string;
};
+type CreateVaultServiceAccountResult = RotateVaultServiceAccountResult & {
+ /** The project's default server wallet, null when none was created. */
+ projectWalletAddress: string | null;
+};
+
/**
* Credentials the caller supplies to unlock a vault. A managed vault takes the
* project secret key, an ejected one takes the vault admin key.
@@ -153,6 +158,38 @@ export async function setVaultProjectWallet(params: {
return res.data.data;
}
+/**
+ * Creates the project's vault service account and its access tokens.
+ *
+ * A managed vault seals its admin key and wallet token with the project secret
+ * key and returns neither. An ejected vault returns both, once.
+ */
+export async function createVaultServiceAccount(params: {
+ project: ProjectRef;
+ mode: "managed" | "ejected";
+ projectSecretKey?: string;
+ skipWalletCreation?: boolean;
+}): Promise {
+ const res = await apiServerProxy<{
+ data: CreateVaultServiceAccountResult;
+ }>({
+ body: JSON.stringify({
+ mode: params.mode,
+ projectSecretKey: params.projectSecretKey,
+ skipWalletCreation: params.skipWalletCreation,
+ }),
+ headers: { "Content-Type": "application/json" },
+ method: "POST",
+ pathname: vaultPath(params.project, "/service-account"),
+ });
+
+ if (!res.ok) {
+ throw new Error(errorMessage(res.error, "Failed to create vault"));
+ }
+
+ return res.data.data;
+}
+
export async function rotateVaultServiceAccount(params: {
project: ProjectRef;
projectSecretKey?: string;
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 1dec37b0d5d..6fc0b594e97 100644
--- a/apps/dashboard/src/@/components/project/create-project-modal/index.tsx
+++ b/apps/dashboard/src/@/components/project/create-project-modal/index.tsx
@@ -7,6 +7,7 @@ import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
+import { createVaultServiceAccount } from "@/actions/vault";
import type { Project } from "@/api/project/projects";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
@@ -38,7 +39,6 @@ import { createProjectClient } from "@/hooks/useApi";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import { projectDomainsSchema, projectNameSchema } from "@/schema/validations";
import { toArrFromList } from "@/utils/string";
-import { createVaultAccountAndAccessToken } from "../../../../app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/lib/vault.client";
const ALL_PROJECT_SERVICES = SERVICES.filter(
(srv) => srv.name !== "relayer" && srv.name !== "chainsaw",
@@ -64,8 +64,12 @@ const CreateProjectDialog = (props: CreateProjectDialogProps) => {
{
const res = await createProjectClient(props.teamId, params);
- const vaultTokens = await createVaultAccountAndAccessToken({
- project: res.project,
+ await createVaultServiceAccount({
+ mode: "managed",
+ project: {
+ projectId: res.project.id,
+ teamId: res.project.teamId,
+ },
projectSecretKey: res.secret,
}).catch((error) => {
console.error(
@@ -75,12 +79,6 @@ const CreateProjectDialog = (props: CreateProjectDialogProps) => {
throw error;
});
- const managementAccessToken = vaultTokens.managementToken?.accessToken;
-
- if (!managementAccessToken) {
- throw new Error("Missing management access token for project wallet");
- }
-
return {
project: res.project,
secret: res.secret,
diff --git a/apps/dashboard/src/app/(app)/get-started/team/[team_slug]/create-project/_components/create-project-form.tsx b/apps/dashboard/src/app/(app)/get-started/team/[team_slug]/create-project/_components/create-project-form.tsx
index c5406c9ac57..8924557d0c2 100644
--- a/apps/dashboard/src/app/(app)/get-started/team/[team_slug]/create-project/_components/create-project-form.tsx
+++ b/apps/dashboard/src/app/(app)/get-started/team/[team_slug]/create-project/_components/create-project-form.tsx
@@ -8,6 +8,7 @@ import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
+import { createVaultServiceAccount } from "@/actions/vault";
import { reportOnboardingCompleted } from "@/analytics/report";
import type { Project } from "@/api/project/projects";
import type { CreateProjectPrefillOptions } from "@/components/project/create-project-modal";
@@ -33,7 +34,6 @@ import { createProjectClient } from "@/hooks/useApi";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import { projectDomainsSchema, projectNameSchema } from "@/schema/validations";
import { toArrFromList } from "@/utils/string";
-import { createVaultAccountAndAccessToken } from "../../../../../team/[team_slug]/[project_slug]/(sidebar)/transactions/lib/vault.client";
const ALL_PROJECT_SERVICES = SERVICES.filter(
(srv) => srv.name !== "relayer" && srv.name !== "chainsaw",
@@ -55,8 +55,12 @@ export function CreateProjectFormOnboarding(props: {
{
const res = await createProjectClient(props.teamId, params);
- const vaultTokens = await createVaultAccountAndAccessToken({
- project: res.project,
+ await createVaultServiceAccount({
+ mode: "managed",
+ project: {
+ projectId: res.project.id,
+ teamId: res.project.teamId,
+ },
projectSecretKey: res.secret,
}).catch((error) => {
console.error(
@@ -66,15 +70,6 @@ export function CreateProjectFormOnboarding(props: {
throw error;
});
- const managementAccessToken =
- vaultTokens.managementToken?.accessToken;
-
- if (!managementAccessToken) {
- throw new Error(
- "Missing management access token for project wallet",
- );
- }
-
return {
project: res.project,
secret: res.secret,
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 5315381c7b1..41c7728c41c 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
@@ -3,12 +3,9 @@
import { decrypt, encrypt } from "@thirdweb-dev/service-utils";
import {
createAccessToken,
- createEoa,
- createServiceAccount,
createVaultClient,
type VaultClient,
} from "@thirdweb-dev/vault-sdk";
-import { engineCloudProxy } from "@/actions/proxies";
import {
createVaultServerWallet,
setVaultProjectWallet,
@@ -16,7 +13,6 @@ import {
import type { Project } from "@/api/project/projects";
import { NEXT_PUBLIC_THIRDWEB_VAULT_URL } from "@/constants/public-envs";
import { updateProjectClient } from "@/hooks/useApi";
-import { getProjectWalletLabel } from "@/lib/project-wallet";
const SERVER_WALLET_ACCESS_TOKEN_PURPOSE =
"Access Token for All Server Wallets";
@@ -24,44 +20,11 @@ const SERVER_WALLET_ACCESS_TOKEN_PURPOSE =
const SERVER_WALLET_MANAGEMENT_ACCESS_TOKEN_PURPOSE =
"Management Token for Dashboard";
-/**
- * Retry a function with exponential backoff.
- */
-async function withRetry(
- fn: () => Promise,
- options: { maxAttempts?: number; baseDelayMs?: number } = {},
-): Promise {
- const { maxAttempts = 3, baseDelayMs = 1000 } = options;
- if (!Number.isInteger(maxAttempts) || maxAttempts < 1) {
- throw new Error("maxAttempts must be at least 1");
- }
- if (!Number.isFinite(baseDelayMs) || baseDelayMs < 0) {
- throw new Error("baseDelayMs must be a non-negative number");
- }
- let lastError: Error | undefined;
-
- for (let attempt = 1; attempt <= maxAttempts; attempt++) {
- try {
- return await fn();
- } catch (error) {
- lastError = error instanceof Error ? error : new Error(String(error));
- if (attempt < maxAttempts) {
- // Exponential backoff with cap at 30s and jitter to prevent thundering herd
- const delay = Math.min(baseDelayMs * 2 ** (attempt - 1), 30_000);
- const jitter = Math.floor(Math.random() * Math.min(250, delay));
- await new Promise((resolve) =>
- setTimeout(resolve, delay + jitter),
- );
- }
- }
- }
-
- throw lastError ?? new Error("withRetry failed without capturing an error");
-}
-
let vc: VaultClient | null = null;
-export async function initVaultClient() {
+// Only `upgradeAccessTokensForSolana` still needs this; every other vault
+// operation goes through api-server.
+async function initVaultClient() {
if (vc) {
return vc;
}
@@ -71,57 +34,6 @@ export async function initVaultClient() {
return vc;
}
-export async function createVaultAccountAndAccessToken(props: {
- project: Project;
- projectSecretKey?: string;
- projectSecretHash?: string;
-}) {
- try {
- const vaultClient = await initVaultClient();
-
- 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;
- const adminKey = serviceAccount.adminKey;
- const rotationCode = serviceAccount.rotationCode;
-
- const { managementToken, walletToken } =
- await createAndEncryptVaultAccessTokens({
- project: props.project,
- projectSecretKey: props.projectSecretKey,
- projectSecretHash: props.projectSecretHash,
- vaultClient,
- adminKey,
- rotationCode,
- });
-
- return {
- adminKey,
- managementToken,
- walletToken,
- };
- } catch (error) {
- throw new Error(
- `Failed to create vault account and access token: ${error}`,
- );
- }
-}
-
export async function createProjectServerWallet(props: {
project: Project;
label?: string;
@@ -147,166 +59,6 @@ export async function updateDefaultProjectWallet(props: {
});
}
-/**
- * Creates the project's first server wallet during vault setup. Runs before the
- * management token is stored on the project, so it uses the token it was handed.
- */
-async function createDefaultServerWallet(props: {
- project: Project;
- managementAccessToken: string;
- vaultClient: VaultClient;
-}) {
- const eoa = await createEoa({
- client: props.vaultClient,
- request: {
- auth: {
- accessToken: props.managementAccessToken,
- },
- options: {
- metadata: {
- label: getProjectWalletLabel(props.project.name),
- projectId: props.project.id,
- teamId: props.project.teamId,
- type: "server-wallet",
- },
- },
- },
- });
-
- if (!eoa.success) {
- throw new Error(eoa.error?.message || "Failed to create server wallet");
- }
-
- engineCloudProxy({
- body: JSON.stringify({
- signerAddress: eoa.data.address,
- }),
- headers: {
- "Content-Type": "application/json",
- "x-client-id": props.project.publishableKey,
- "x-team-id": props.project.teamId,
- },
- method: "POST",
- pathname: "/cache/smart-account",
- }).catch((err) => {
- console.warn("failed to cache server wallet", err);
- });
-
- return eoa.data;
-}
-
-async function createAndEncryptVaultAccessTokens(props: {
- project: Project;
- vaultClient: VaultClient;
- projectSecretKey?: string;
- projectSecretHash?: string;
- adminKey: string;
- rotationCode: string;
-}) {
- const {
- project,
- projectSecretKey,
- projectSecretHash,
- vaultClient,
- adminKey,
- rotationCode,
- } = props;
-
- const [managementTokenResult, walletTokenResult] = await Promise.all([
- createManagementAccessToken({ project, adminKey, vaultClient }),
- createWalletAccessToken({ project, adminKey, vaultClient }),
- ]);
-
- if (!managementTokenResult.success) {
- throw new Error(
- `Failed to create management token: ${managementTokenResult.error}`,
- );
- }
-
- if (!walletTokenResult.success) {
- throw new Error(
- `Failed to create wallet token: ${walletTokenResult.error}`,
- );
- }
-
- const managementToken = managementTokenResult.data;
- const walletToken = walletTokenResult.data;
-
- // CRITICAL: Save credentials IMMEDIATELY after creating tokens.
- // This prevents a broken state if wallet creation or other operations fail.
- // The rotationCode is consumed when rotating, so if we don't save the new one,
- // the project becomes unrecoverable.
- let encryptedAdminKey: string | null = null;
- let encryptedWalletAccessToken: string | null = null;
-
- if (projectSecretKey) {
- // verify that the project secret key is valid
- const projectSecretKeyHash = await hashSecretKey(projectSecretKey);
- const secretKeysHashed = [
- ...project.secretKeys,
- // for newly rotated secret keys, we don't have the secret key in the project secret keys yet
- ...(projectSecretHash ? [{ hash: projectSecretHash }] : []),
- ];
- if (!secretKeysHashed.some((key) => key?.hash === projectSecretKeyHash)) {
- throw new Error("Invalid project secret key");
- }
-
- // encrypt admin key and wallet token with project secret key
- [encryptedAdminKey, encryptedWalletAccessToken] = await Promise.all([
- encrypt(adminKey, projectSecretKey),
- encrypt(walletToken.accessToken, projectSecretKey),
- ]);
- }
-
- let projectWalletAddress: string | null | undefined = project.services.find(
- (s) => s.name === "engineCloud",
- )?.projectWalletAddress;
-
- if (!projectWalletAddress) {
- const defaultProjectServerWallet = await createDefaultServerWallet({
- project,
- managementAccessToken: managementToken.accessToken,
- vaultClient,
- });
- projectWalletAddress = defaultProjectServerWallet.address;
- }
-
- // Save credentials with retry - this is critical because if rotation succeeded
- // but this save fails, the new rotation code is lost and project becomes unrecoverable
- await withRetry(
- () =>
- updateProjectClient(
- {
- projectId: project.id,
- teamId: project.teamId,
- },
- {
- services: [
- ...project.services.filter(
- (service) => service.name !== "engineCloud",
- ),
- {
- name: "engineCloud",
- actions: [],
- managementAccessToken: managementToken.accessToken,
- maskedAdminKey: maskSecret(adminKey),
- encryptedAdminKey,
- encryptedWalletAccessToken,
- rotationCode: rotationCode,
- projectWalletAddress: projectWalletAddress ?? null,
- },
- ],
- },
- ),
- { maxAttempts: 3, baseDelayMs: 1000 },
- );
-
- return {
- managementToken,
- walletToken,
- };
-}
-
async function createWalletAccessToken(props: {
project: Project;
adminKey: string;
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 4bfd99bcf84..56bea93f967 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
@@ -9,6 +9,7 @@ import {
} from "lucide-react";
import { useState } from "react";
import { toast } from "sonner";
+import { createVaultServiceAccount } from "@/actions/vault";
import type { Project } from "@/api/project/projects";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
@@ -25,10 +26,7 @@ import { Label } from "@/components/ui/label";
import { Spinner } from "@/components/ui/Spinner";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import { cn } from "@/lib/utils";
-import {
- createVaultAccountAndAccessToken,
- maskSecret,
-} from "../../transactions/lib/vault.client";
+import { maskSecret } from "../../transactions/lib/vault.client";
export function CreateVaultAccountButton(props: { project: Project }) {
const [secretKeyModalOpen, setSecretKeyModalOpen] = useState(false);
@@ -45,12 +43,14 @@ export function CreateVaultAccountButton(props: { project: Project }) {
setModalOpen(true);
setSecretKeyModalOpen(false);
- const result = await createVaultAccountAndAccessToken({
- project: props.project,
+ return createVaultServiceAccount({
+ mode: projectSecretKey ? "managed" : "ejected",
+ project: {
+ projectId: props.project.id,
+ teamId: props.project.teamId,
+ },
projectSecretKey,
});
-
- return result;
},
onError: (error) => {
setErrorMessage(error.message);
@@ -90,12 +90,23 @@ export function CreateVaultAccountButton(props: { project: Project }) {
setManageSelfChecked(false);
};
+ // Only an ejected vault returns its keys; a managed vault keeps them.
+ const createdKeys =
+ initialiseProjectWithVaultMutation.data?.adminKey &&
+ initialiseProjectWithVaultMutation.data.walletAccessToken
+ ? {
+ adminKey: initialiseProjectWithVaultMutation.data.adminKey,
+ walletAccessToken:
+ initialiseProjectWithVaultMutation.data.walletAccessToken,
+ }
+ : undefined;
+
const handleDownloadKeys = () => {
- if (!initialiseProjectWithVaultMutation.data) {
+ if (!createdKeys) {
return;
}
- const fileContent = `Project:\n${props.project.name} (${props.project.publishableKey})\n\nVault Admin Key:\n${initialiseProjectWithVaultMutation.data.adminKey}\n\nVault Access Token:\n${initialiseProjectWithVaultMutation.data.walletToken.accessToken}\n`;
+ const fileContent = `Project:\n${props.project.name} (${props.project.publishableKey})\n\nVault Admin Key:\n${createdKeys.adminKey}\n\nVault Access Token:\n${createdKeys.walletAccessToken}\n`;
const blob = new Blob([fileContent], { type: "text/plain;charset=utf-8" });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
@@ -114,7 +125,7 @@ export function CreateVaultAccountButton(props: { project: Project }) {
};
const handleCloseModal = () => {
- if (!keysConfirmed) {
+ if (createdKeys && !keysConfirmed) {
return;
}
@@ -234,7 +245,7 @@ export function CreateVaultAccountButton(props: { project: Project }) {
>
- ) : initialiseProjectWithVaultMutation.data ? (
+ ) : createdKeys ? (
Save your Vault Admin Key
@@ -265,12 +276,8 @@ export function CreateVaultAccountButton(props: { project: Project }) {
@@ -323,6 +330,34 @@ export function CreateVaultAccountButton(props: { project: Project }) {
+ ) : initialiseProjectWithVaultMutation.data ? (
+
+
+ Vault Created
+
+
+
+
+
Vault Admin Key
+
+
+ {initialiseProjectWithVaultMutation.data.maskedAdminKey}
+
+
+ Your admin key and wallet access token were encrypted with
+ your project secret key. Use your project secret key to
+ access your vault.
+
+
+
+
+
+
+
+
+
) : null}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/server-wallets/wallets/vault-recovery-card.client.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/server-wallets/wallets/vault-recovery-card.client.tsx
index 619992f67a3..d6400c8c295 100644
--- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/server-wallets/wallets/vault-recovery-card.client.tsx
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/server-wallets/wallets/vault-recovery-card.client.tsx
@@ -9,6 +9,7 @@ import {
} from "lucide-react";
import { useState } from "react";
import { toast } from "sonner";
+import { createVaultServiceAccount } from "@/actions/vault";
import type { Project } from "@/api/project/projects";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import {
@@ -27,11 +28,7 @@ import { CopyTextButton } from "@/components/ui/CopyTextButton";
import { Checkbox, CheckboxWithLabel } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input";
import { Spinner } from "@/components/ui/Spinner";
-import {
- createVaultAccountAndAccessToken,
- initVaultClient,
- maskSecret,
-} from "../../../transactions/lib/vault.client";
+import { maskSecret } from "../../../transactions/lib/vault.client";
interface VaultRecoveryCardProps {
errorMessage: string;
@@ -66,15 +63,12 @@ export function VaultRecoveryCard({
const regenerateMutation = useMutation({
mutationFn: async () => {
- await initVaultClient();
-
- const result = await createVaultAccountAndAccessToken({
- project,
+ return createVaultServiceAccount({
+ mode: willCreateEjectedVault ? "ejected" : "managed",
+ project: { projectId: project.id, teamId: project.teamId },
// Only pass secret key if creating managed vault (not ejected)
projectSecretKey: willCreateEjectedVault ? undefined : secretKeyInput,
});
-
- return result;
},
onSuccess: () => {
// For managed vaults (with secret key), reload immediately
@@ -86,12 +80,22 @@ export function VaultRecoveryCard({
},
});
+ // Only an ejected vault returns its keys; a managed vault keeps them.
+ const createdKeys =
+ regenerateMutation.data?.adminKey &&
+ regenerateMutation.data.walletAccessToken
+ ? {
+ adminKey: regenerateMutation.data.adminKey,
+ walletAccessToken: regenerateMutation.data.walletAccessToken,
+ }
+ : undefined;
+
const handleDownloadKeys = () => {
- if (!regenerateMutation.data) {
+ if (!createdKeys) {
return;
}
- const fileContent = `Project:\n${project.name} (${project.publishableKey})\n\nVault Admin Key:\n${regenerateMutation.data.adminKey}\n\nVault Access Token:\n${regenerateMutation.data.walletToken.accessToken}\n`;
+ const fileContent = `Project:\n${project.name} (${project.publishableKey})\n\nVault Admin Key:\n${createdKeys.adminKey}\n\nVault Access Token:\n${createdKeys.walletAccessToken}\n`;
const blob = new Blob([fileContent], { type: "text/plain;charset=utf-8" });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
@@ -170,7 +174,7 @@ export function VaultRecoveryCard({
{/* Show key download UI for ejected vaults after success */}
- {willCreateEjectedVault && regenerateMutation.data ? (
+ {createdKeys ? (
<>
Save your Vault Admin Key
@@ -185,10 +189,8 @@ export function VaultRecoveryCard({