From 7140f31a9c5fd558789eb9cee55c45a2cca75307 Mon Sep 17 00:00:00 2001 From: 0xFirekeeper <0xFirekeeper@gmail.com> Date: Thu, 27 Aug 2026 09:54:32 +0700 Subject: [PATCH] [Dashboard] Remove the Solana access-token upgrade path The one-off upgrade that reissued a project's vault access tokens in place to add Solana scopes is no longer needed: rotating the admin key reissues both tokens under the current policy set, which already covers Solana. Dropping its last caller leaves no client-side vault usage, so the vault client, its base URL env var, and the vault SDK dependency go with it. Co-Authored-By: Claude Fable 5 --- apps/dashboard/.env.example | 1 - apps/dashboard/knip.json | 1 - apps/dashboard/package.json | 1 - apps/dashboard/src/@/constants/public-envs.ts | 3 - .../server-wallets-table.client.tsx | 17 +- .../transactions/lib/vault.client.ts | 672 ------------------ .../upgrade-solana-permissions.client.tsx | 218 ------ pnpm-lock.yaml | 3 - 8 files changed, 15 insertions(+), 901 deletions(-) delete mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/solana-wallets/components/upgrade-solana-permissions.client.tsx diff --git a/apps/dashboard/.env.example b/apps/dashboard/.env.example index 918d54845d4..b995bdcd0a7 100644 --- a/apps/dashboard/.env.example +++ b/apps/dashboard/.env.example @@ -33,7 +33,6 @@ NEXT_PUBLIC_TYPESENSE_CONTRACT_API_KEY= NEXT_PUBLIC_POSTHOG_KEY="" # required for server wallet management -NEXT_PUBLIC_THIRDWEB_VAULT_URL="" NEXT_PUBLIC_ENGINE_CLOUD_URL="" diff --git a/apps/dashboard/knip.json b/apps/dashboard/knip.json index 03076122bc2..aa7cddb28e7 100644 --- a/apps/dashboard/knip.json +++ b/apps/dashboard/knip.json @@ -10,7 +10,6 @@ "ignoreDependencies": [ "@thirdweb-dev/api", "@thirdweb-dev/service-utils", - "@thirdweb-dev/vault-sdk", "thirdweb", "@types/color", "fast-xml-parser", diff --git a/apps/dashboard/package.json b/apps/dashboard/package.json index 7d28b3d88fd..de5c8a7f41a 100644 --- a/apps/dashboard/package.json +++ b/apps/dashboard/package.json @@ -28,7 +28,6 @@ "@tanstack/react-table": "^8.21.3", "@thirdweb-dev/api": "workspace:*", "@thirdweb-dev/service-utils": "workspace:*", - "@thirdweb-dev/vault-sdk": "workspace:*", "@vercel/functions": "2.2.2", "@vercel/og": "^0.6.8", "@workspace/ui": "workspace:*", diff --git a/apps/dashboard/src/@/constants/public-envs.ts b/apps/dashboard/src/@/constants/public-envs.ts index 80f2d5d965a..fe471c31f92 100644 --- a/apps/dashboard/src/@/constants/public-envs.ts +++ b/apps/dashboard/src/@/constants/public-envs.ts @@ -1,9 +1,6 @@ export const NEXT_PUBLIC_DASHBOARD_CLIENT_ID = process.env.NEXT_PUBLIC_DASHBOARD_CLIENT_ID || ""; -export const NEXT_PUBLIC_THIRDWEB_VAULT_URL = - process.env.NEXT_PUBLIC_THIRDWEB_VAULT_URL || ""; - export const NEXT_PUBLIC_ENGINE_CLOUD_URL = process.env.NEXT_PUBLIC_ENGINE_CLOUD_URL || ""; 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 d47cdeefa60..7122e0fc917 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 @@ -63,7 +63,6 @@ import { updateDefaultProjectWallet } from "../lib/vault.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"; import type { SolanaWallet } from "../solana-wallets/wallet-table/types"; type WalletChain = "evm" | "solana"; @@ -238,7 +237,21 @@ export function ServerWalletsTable(props: ServerWalletsTableProps) { {/* Table Content */} {activeChain === "solana" && solanaPermissionError ? (
- +
+

+ Solana access not enabled +

+

+ This project's vault access tokens were issued before Solana + support existed. Rotate the admin key to reissue them with + Solana permissions. +

+ +
) : ( <> 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 41c7728c41c..577bc3959b2 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 @@ -1,38 +1,10 @@ "use client"; -import { decrypt, encrypt } from "@thirdweb-dev/service-utils"; -import { - createAccessToken, - createVaultClient, - type VaultClient, -} from "@thirdweb-dev/vault-sdk"; import { createVaultServerWallet, setVaultProjectWallet, } from "@/actions/vault"; import type { Project } from "@/api/project/projects"; -import { NEXT_PUBLIC_THIRDWEB_VAULT_URL } from "@/constants/public-envs"; -import { updateProjectClient } from "@/hooks/useApi"; - -const SERVER_WALLET_ACCESS_TOKEN_PURPOSE = - "Access Token for All Server Wallets"; - -const SERVER_WALLET_MANAGEMENT_ACCESS_TOKEN_PURPOSE = - "Management Token for Dashboard"; - -let vc: VaultClient | null = null; - -// Only `upgradeAccessTokensForSolana` still needs this; every other vault -// operation goes through api-server. -async function initVaultClient() { - if (vc) { - return vc; - } - vc = await createVaultClient({ - baseUrl: NEXT_PUBLIC_THIRDWEB_VAULT_URL, - }); - return vc; -} export async function createProjectServerWallet(props: { project: Project; @@ -59,650 +31,6 @@ export async function updateDefaultProjectWallet(props: { }); } -async function createWalletAccessToken(props: { - project: Project; - adminKey: string; - vaultClient: VaultClient; -}) { - return createAccessToken({ - client: props.vaultClient, - request: { - auth: { - adminKey: props.adminKey, - }, - options: { - expiresAt: new Date( - Date.now() + 60 * 60 * 1000 * 24 * 365 * 1000, - ).toISOString(), // 100 years from now - metadata: { - projectId: props.project.id, - purpose: SERVER_WALLET_ACCESS_TOKEN_PURPOSE, - teamId: props.project.teamId, - }, - policies: [ - { - metadataPatterns: [ - { - key: "projectId", - rule: { - pattern: props.project.id, - }, - }, - { - key: "teamId", - rule: { - pattern: props.project.teamId, - }, - }, - { - key: "type", - rule: { - pattern: "server-wallet", - }, - }, - ], - type: "eoa:read", - }, - { - requiredMetadataPatterns: [ - { - key: "projectId", - rule: { - pattern: props.project.id, - }, - }, - { - key: "teamId", - rule: { - pattern: props.project.teamId, - }, - }, - { - key: "type", - rule: { - pattern: "server-wallet", - }, - }, - ], - type: "eoa:create", - }, - { - metadataPatterns: [ - { - key: "projectId", - rule: { - pattern: props.project.id, - }, - }, - { - key: "teamId", - rule: { - pattern: props.project.teamId, - }, - }, - { - key: "type", - rule: { - pattern: "server-wallet", - }, - }, - ], - type: "eoa:signMessage", - }, - { - metadataPatterns: [ - { - key: "projectId", - rule: { - pattern: props.project.id, - }, - }, - { - key: "teamId", - rule: { - pattern: props.project.teamId, - }, - }, - { - key: "type", - rule: { - pattern: "server-wallet", - }, - }, - ], - payloadPatterns: {}, - type: "eoa:signTransaction", - }, - { - metadataPatterns: [ - { - key: "projectId", - rule: { - pattern: props.project.id, - }, - }, - { - key: "teamId", - rule: { - pattern: props.project.teamId, - }, - }, - { - key: "type", - rule: { - pattern: "server-wallet", - }, - }, - ], - type: "eoa:signTypedData", - }, - { - metadataPatterns: [ - { - key: "projectId", - rule: { - pattern: props.project.id, - }, - }, - { - key: "teamId", - rule: { - pattern: props.project.teamId, - }, - }, - { - key: "type", - rule: { - pattern: "server-wallet", - }, - }, - ], - type: "eoa:signAuthorization", - }, - { - metadataPatterns: [ - { - key: "projectId", - rule: { - pattern: props.project.id, - }, - }, - { - key: "teamId", - rule: { - pattern: props.project.teamId, - }, - }, - { - key: "type", - rule: { - pattern: "server-wallet", - }, - }, - ], - structuredPatterns: { - useropV06: {}, - useropV07: {}, - }, - type: "eoa:signStructuredMessage", - }, - { - requiredMetadataPatterns: [ - { - key: "projectId", - rule: { - pattern: props.project.id, - }, - }, - { - key: "teamId", - rule: { - pattern: props.project.teamId, - }, - }, - { - key: "type", - rule: { - pattern: "server-wallet", - }, - }, - ], - type: "eoa:create", - }, - { - metadataPatterns: [ - { - key: "projectId", - rule: { - pattern: props.project.id, - }, - }, - { - key: "teamId", - rule: { - pattern: props.project.teamId, - }, - }, - { - key: "type", - rule: { - pattern: "server-wallet", - }, - }, - ], - type: "solana:read", - }, - { - requiredMetadataPatterns: [ - { - key: "projectId", - rule: { - pattern: props.project.id, - }, - }, - { - key: "teamId", - rule: { - pattern: props.project.teamId, - }, - }, - { - key: "type", - rule: { - pattern: "server-wallet", - }, - }, - ], - type: "solana:create", - }, - { - metadataPatterns: [ - { - key: "projectId", - rule: { - pattern: props.project.id, - }, - }, - { - key: "teamId", - rule: { - pattern: props.project.teamId, - }, - }, - { - key: "type", - rule: { - pattern: "server-wallet", - }, - }, - ], - type: "solana:signTransaction", - }, - { - metadataPatterns: [ - { - key: "projectId", - rule: { - pattern: props.project.id, - }, - }, - { - key: "teamId", - rule: { - pattern: props.project.teamId, - }, - }, - { - key: "type", - rule: { - pattern: "server-wallet", - }, - }, - ], - type: "solana:signMessage", - }, - ], - }, - }, - }); -} - -async function createManagementAccessToken(props: { - project: Project; - adminKey: string; - vaultClient: VaultClient; -}) { - return createAccessToken({ - client: props.vaultClient, - request: { - auth: { - adminKey: props.adminKey, - }, - options: { - expiresAt: new Date( - Date.now() + 60 * 60 * 1000 * 24 * 365 * 1000, - ).toISOString(), // 100 years from now - metadata: { - projectId: props.project.id, - purpose: SERVER_WALLET_MANAGEMENT_ACCESS_TOKEN_PURPOSE, - teamId: props.project.teamId, - }, - policies: [ - { - metadataPatterns: [ - { - key: "projectId", - rule: { - pattern: props.project.id, - }, - }, - { - key: "teamId", - rule: { - pattern: props.project.teamId, - }, - }, - { - key: "type", - rule: { - pattern: "server-wallet", - }, - }, - ], - type: "eoa:read", - }, - { - requiredMetadataPatterns: [ - { - key: "projectId", - rule: { - pattern: props.project.id, - }, - }, - { - key: "teamId", - rule: { - pattern: props.project.teamId, - }, - }, - { - key: "type", - rule: { - pattern: "server-wallet", - }, - }, - ], - type: "eoa:create", - }, - { - metadataPatterns: [ - { - key: "projectId", - rule: { - pattern: props.project.id, - }, - }, - { - key: "teamId", - rule: { - pattern: props.project.teamId, - }, - }, - { - key: "type", - rule: { - pattern: "server-wallet", - }, - }, - ], - type: "solana:read", - }, - { - requiredMetadataPatterns: [ - { - key: "projectId", - rule: { - pattern: props.project.id, - }, - }, - { - key: "teamId", - rule: { - pattern: props.project.teamId, - }, - }, - { - key: "type", - rule: { - pattern: "server-wallet", - }, - }, - ], - type: "solana:create", - }, - { - metadataPatterns: [ - { - key: "projectId", - rule: { - pattern: props.project.id, - }, - }, - { - key: "teamId", - rule: { - pattern: props.project.teamId, - }, - }, - ], - revealSensitive: false, - type: "accessToken:read", - }, - ], - }, - }, - }); -} - -/** - * Upgrades existing access tokens to include Solana permissions - * This is needed when a project was created before Solana support was added - * - * Returns an object with success/error instead of throwing for Next.js server actions - */ -export async function upgradeAccessTokensForSolana(props: { - project: Project; - projectSecretKey?: string; -}): Promise<{ - success: boolean; - error?: string; - data?: { - managementToken: string; - walletToken?: string; - }; -}> { - const { project, projectSecretKey } = props; - - try { - // Find the engineCloud service - const engineCloudService = project.services.find( - (service) => service.name === "engineCloud", - ); - - if (!engineCloudService) { - return { - success: false, - error: "No engineCloud service found on project", - }; - } - - const vaultClient = await initVaultClient(); - const hasEncryptedAdminKey = !!engineCloudService.encryptedAdminKey; - - // Check if this is an ejected vault (no encrypted admin key stored) - if (!hasEncryptedAdminKey) { - // For ejected vaults, we only need to update the management token - // User manages their own admin key, so we can't create wallet tokens - - // We need the admin key from the user - if (!projectSecretKey) { - return { - success: false, - error: "Admin key required. Please enter your vault admin key.", - }; - } - - // For ejected vault, the "secret key" parameter is actually the admin key - const managementTokenResult = await createManagementAccessToken({ - project, - adminKey: projectSecretKey, - vaultClient, - }); - - if (!managementTokenResult.success) { - return { - success: false, - error: `Failed to create management token: ${managementTokenResult.error}`, - }; - } - - // Update only the management token for ejected vaults - // Keep everything else the same (no encrypted keys to update) - await updateProjectClient( - { - projectId: project.id, - teamId: project.teamId, - }, - { - services: [ - ...project.services.filter( - (service) => service.name !== "engineCloud", - ), - { - ...engineCloudService, - managementAccessToken: managementTokenResult.data.accessToken, - }, - ], - }, - ); - - return { - success: true, - data: { - managementToken: managementTokenResult.data.accessToken, - }, - }; - } - - // For non-ejected vaults (with encrypted admin key) - if (!projectSecretKey) { - return { - success: false, - error: "Project secret key is required to upgrade tokens", - }; - } - - // Verify the project secret key - const projectSecretKeyHash = await hashSecretKey(projectSecretKey); - if (!project.secretKeys.some((key) => key?.hash === projectSecretKeyHash)) { - return { - success: false, - error: "Invalid project secret key", - }; - } - - // Decrypt the admin key (we know it exists from the hasEncryptedAdminKey check) - const adminKey = await decrypt( - engineCloudService.encryptedAdminKey as string, - projectSecretKey, - ); - - // Create new tokens with Solana permissions - const [managementTokenResult, walletTokenResult] = await Promise.all([ - createManagementAccessToken({ project, adminKey, vaultClient }), - createWalletAccessToken({ project, adminKey, vaultClient }), - ]); - - if (!managementTokenResult.success) { - return { - success: false, - error: `Failed to create management token: ${managementTokenResult.error}`, - }; - } - - if (!walletTokenResult.success) { - return { - success: false, - error: `Failed to create wallet token: ${walletTokenResult.error}`, - }; - } - - const managementToken = managementTokenResult.data; - const walletToken = walletTokenResult.data; - - // Encrypt the new wallet token - const [encryptedAdminKey, encryptedWalletAccessToken] = await Promise.all([ - encrypt(adminKey, projectSecretKey), - encrypt(walletToken.accessToken, projectSecretKey), - ]); - - // Update the project with new tokens - await updateProjectClient( - { - projectId: project.id, - teamId: project.teamId, - }, - { - services: [ - ...project.services.filter( - (service) => service.name !== "engineCloud", - ), - { - ...engineCloudService, - managementAccessToken: managementToken.accessToken, - encryptedAdminKey, - encryptedWalletAccessToken, - }, - ], - }, - ); - - return { - success: true, - data: { - managementToken: managementToken.accessToken, - walletToken: walletToken.accessToken, - }, - }; - } catch (error) { - return { - success: false, - error: - error instanceof Error - ? error.message - : "Failed to upgrade access tokens", - }; - } -} - export function maskSecret(secret: string) { return `${secret.substring(0, 11)}...${secret.substring(secret.length - 5)}`; } - -async function hashSecretKey(secretKey: string) { - if (typeof window === "undefined" || !window.crypto?.subtle) { - throw new Error( - "This function can only be used in the browser environment with Web Crypto API support.", - ); - } - const encoder = new TextEncoder(); - const data = encoder.encode(secretKey); - const hashBuffer = await window.crypto.subtle.digest("SHA-256", data); - return bufferToHex(hashBuffer); -} - -function bufferToHex(buffer: ArrayBuffer) { - return [...new Uint8Array(buffer)] - .map((x) => x.toString(16).padStart(2, "0")) - .join(""); -} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/solana-wallets/components/upgrade-solana-permissions.client.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/solana-wallets/components/upgrade-solana-permissions.client.tsx deleted file mode 100644 index dfa0ff77b2e..00000000000 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/solana-wallets/components/upgrade-solana-permissions.client.tsx +++ /dev/null @@ -1,218 +0,0 @@ -"use client"; - -import { zodResolver } from "@hookform/resolvers/zod"; -import { useMutation } from "@tanstack/react-query"; -import { KeyRoundIcon, Loader2Icon, ShieldCheckIcon } from "lucide-react"; -import { useState } from "react"; -import { useForm } from "react-hook-form"; -import { toast } from "sonner"; -import * as z from "zod"; -import type { Project } from "@/api/project/projects"; -import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; -import { Button } from "@/components/ui/button"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { useDashboardRouter } from "@/lib/DashboardRouter"; -import { upgradeAccessTokensForSolana } from "../../lib/vault.client"; - -const formSchema = z.object({ - secretKey: z.string().min(1, "Secret key is required"), -}); - -type FormValues = z.infer; - -export function UpgradeSolanaPermissions(props: { project: Project }) { - const router = useDashboardRouter(); - const [isComplete, setIsComplete] = useState(false); - - // Check if this is an ejected vault - const engineCloudService = props.project.services.find( - (s) => s.name === "engineCloud", - ); - const isEjectedVault = !engineCloudService?.encryptedAdminKey; - - const form = useForm({ - defaultValues: { - secretKey: "", - }, - resolver: zodResolver(formSchema), - }); - - const upgradeMutation = useMutation({ - mutationFn: async (data: FormValues) => { - return upgradeAccessTokensForSolana({ - project: props.project, - projectSecretKey: data.secretKey, - }); - }, - onSuccess: (result) => { - if (!result.success) { - toast.error(result.error || "Failed to upgrade Solana permissions"); - return; - } - setIsComplete(true); - toast.success("Solana permissions enabled successfully!"); - // Refresh the page after a short delay to show updated state - setTimeout(() => { - router.refresh(); - }, 1500); - }, - onError: (error) => { - toast.error( - error instanceof Error - ? error.message - : "Failed to upgrade Solana permissions", - ); - }, - }); - - if (isComplete) { - return ( - - -
-
- -
-
- - Solana Access Enabled - - - Your project now has full Solana functionality - -
-
-
- -
- -

- Refreshing page to load Solana wallets... -

-
-
-
- ); - } - - return ( - - -
-
- -
-
- Enable Solana Functionality - - Upgrade your project to support Solana wallets and transactions - -
-
-
- - - - {isEjectedVault - ? "Why do you need my admin key?" - : "Why do you need my secret key?"} - - -

- Your project was created before Solana support was added. To - enable Solana features, we need to update your access tokens with - new permissions. -

- {isEjectedVault ? ( - <> -

- Since you're using an ejected vault, your admin key is used - to: -

-
    -
  • Create new access tokens with Solana permissions
  • -
  • Update your management token with the new permissions
  • -
-

- Your admin key is never stored and is only used during this - one-time upgrade process. -

- - ) : ( - <> -

Your secret key is used to:

-
    -
  • Decrypt your existing admin credentials
  • -
  • Create new access tokens with Solana permissions
  • -
  • Re-encrypt and securely store the updated credentials
  • -
-

- Your secret key is never stored and is only used during this - one-time upgrade process. -

- - )} -
-
- -
upgradeMutation.mutate(data))} - > -
- - - {form.formState.errors.secretKey && ( -

- {form.formState.errors.secretKey.message} -

- )} -

- {isEjectedVault - ? "This is the admin key you received when you ejected your vault" - : "You can find your secret key in your project settings"} -

-
- -
- -
-
-
-
- ); -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 79758ec65d5..ef7ffa32d77 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -147,9 +147,6 @@ importers: '@thirdweb-dev/service-utils': specifier: workspace:* version: link:../../packages/service-utils - '@thirdweb-dev/vault-sdk': - specifier: workspace:* - version: link:../../packages/vault-sdk '@vercel/functions': specifier: 2.2.2 version: 2.2.2(@aws-sdk/credential-provider-web-identity@3.840.0)