Skip to content

Commit adbee5c

Browse files
committed
project access token migration
1 parent bc59fdd commit adbee5c

File tree

11 files changed

+489
-599
lines changed

11 files changed

+489
-599
lines changed

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/ProjectSidebarLayout.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,11 @@ export function ProjectSidebarLayout(props: {
4848
{
4949
href: `${props.layoutPath}/transactions`,
5050
icon: ArrowLeftRightIcon,
51-
label: "Transactions",
51+
label: (
52+
<span className="flex items-center gap-2">
53+
Transactions <Badge>New</Badge>
54+
</span>
55+
),
5256
},
5357
{
5458
href: `${props.layoutPath}/contracts`,
@@ -81,11 +85,7 @@ export function ProjectSidebarLayout(props: {
8185
{
8286
href: `${props.layoutPath}/tokens`,
8387
icon: TokenIcon,
84-
label: (
85-
<span className="flex items-center gap-2">
86-
Tokens <Badge>New</Badge>
87-
</span>
88-
),
88+
label: "Tokens",
8989
},
9090
],
9191
},

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/analytics/send-test-solana-tx.client.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
33
import { useMutation, useQueryClient } from "@tanstack/react-query";
44
import { ExternalLinkIcon, Loader2Icon, LockIcon } from "lucide-react";
55
import Link from "next/link";
6+
import { useQueryState } from "nuqs";
67
import { useState } from "react";
78
import { useForm } from "react-hook-form";
89
import { toast } from "sonner";
@@ -50,6 +51,9 @@ function SendTestSolanaTransactionModal(props: {
5051
onOpenChange: (open: boolean) => void;
5152
}) {
5253
const queryClient = useQueryClient();
54+
const [, setTxChain] = useQueryState("txChain", {
55+
history: "push",
56+
});
5357

5458
const form = useForm<FormValues>({
5559
defaultValues: {
@@ -122,6 +126,8 @@ function SendTestSolanaTransactionModal(props: {
122126
},
123127
onSuccess: () => {
124128
toast.success("Test Solana transaction sent successfully!");
129+
// Set the transaction chain to solana so the table switches
130+
setTxChain("solana");
125131
// Close the modal after successful transaction
126132
setTimeout(() => {
127133
props.onOpenChange(false);

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/components/server-wallets-table.client.tsx

Lines changed: 5 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ import { updateDefaultProjectWallet } from "../lib/vault.client";
6161
import CreateServerWallet from "../server-wallets/components/create-server-wallet.client";
6262
import type { Wallet as EVMWallet } from "../server-wallets/wallet-table/types";
6363
import { CreateSolanaWallet } from "../solana-wallets/components/create-solana-wallet.client";
64+
import { UpgradeSolanaPermissions } from "../solana-wallets/components/upgrade-solana-permissions.client";
6465
import type { SolanaWallet } from "../solana-wallets/wallet-table/types";
6566

6667
type WalletChain = "evm" | "solana";
@@ -154,6 +155,7 @@ export function ServerWalletsTable(props: ServerWalletsTableProps) {
154155
managementAccessToken={managementAccessToken}
155156
project={project}
156157
teamSlug={teamSlug}
158+
disabled={solanaPermissionError}
157159
/>
158160
)}
159161
</div>
@@ -211,7 +213,9 @@ export function ServerWalletsTable(props: ServerWalletsTableProps) {
211213

212214
{/* Table Content */}
213215
{activeChain === "solana" && solanaPermissionError ? (
214-
<SolanaPermissionMessage teamSlug={teamSlug} />
216+
<div className="p-6">
217+
<UpgradeSolanaPermissions project={project} />
218+
</div>
215219
) : (
216220
<>
217221
<TableContainer className="rounded-none border-x-0 border-b-0">
@@ -315,32 +319,6 @@ export function ServerWalletsTable(props: ServerWalletsTableProps) {
315319
);
316320
}
317321

318-
// Solana Permission Error Message
319-
function SolanaPermissionMessage({ teamSlug }: { teamSlug: string }) {
320-
return (
321-
<div className="p-8 flex flex-col items-center justify-center text-center gap-4 border-t">
322-
<div className="p-3 rounded-full bg-warning/10 border border-warning/50">
323-
<XIcon className="size-6 text-warning" />
324-
</div>
325-
<div className="max-w-md">
326-
<h3 className="font-semibold text-lg mb-2">
327-
Solana Access Not Available
328-
</h3>
329-
<p className="text-muted-foreground text-sm mb-4">
330-
This project doesn't have access to Solana functionality. To use
331-
Solana server wallets, please create a new project with Solana support
332-
enabled.
333-
</p>
334-
<Link href={`/team/${teamSlug}`}>
335-
<Button variant="default" className="rounded-full">
336-
Create New Project
337-
</Button>
338-
</Link>
339-
</div>
340-
</div>
341-
);
342-
}
343-
344322
// Wallets Pagination Component
345323
function WalletsPagination({
346324
activeChain,

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/components/transactions-table.client.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { keepPreviousData, useQuery } from "@tanstack/react-query";
44
import { format, formatDistanceToNowStrict } from "date-fns";
55
import { ExternalLinkIcon, InfoIcon } from "lucide-react";
66
import Link from "next/link";
7+
import { useQueryState } from "nuqs";
78
import { useId, useState } from "react";
89
import type { ThirdwebClient } from "thirdweb";
910
import { engineCloudProxy } from "@/actions/proxies";
@@ -66,7 +67,17 @@ export function UnifiedTransactionsTable({
6667
client,
6768
}: UnifiedTransactionsTableProps) {
6869
const router = useDashboardRouter();
69-
const [activeChain, setActiveChain] = useState<TransactionChain>("evm");
70+
71+
// Use nuqs to manage chain selection in URL - this is the source of truth
72+
const [activeChainParam, setActiveChainParam] = useQueryState("txChain", {
73+
defaultValue: "evm",
74+
history: "push",
75+
});
76+
77+
const activeChain = (activeChainParam || "evm") as TransactionChain;
78+
const setActiveChain = (chain: TransactionChain) =>
79+
setActiveChainParam(chain);
80+
7081
const [autoUpdate, setAutoUpdate] = useState(true);
7182
const [status, setStatus] = useState<
7283
TransactionStatus | SolanaTransactionStatus | undefined

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/lib/vault.client.ts

Lines changed: 182 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client";
22

3-
import { encrypt } from "@thirdweb-dev/service-utils";
3+
import { decrypt, encrypt } from "@thirdweb-dev/service-utils";
44
import {
55
createAccessToken,
66
createEoa,
@@ -795,6 +795,187 @@ async function createManagementAccessToken(props: {
795795
});
796796
}
797797

798+
/**
799+
* Upgrades existing access tokens to include Solana permissions
800+
* This is needed when a project was created before Solana support was added
801+
*
802+
* Returns an object with success/error instead of throwing for Next.js server actions
803+
*/
804+
export async function upgradeAccessTokensForSolana(props: {
805+
project: Project;
806+
projectSecretKey?: string;
807+
}): Promise<{
808+
success: boolean;
809+
error?: string;
810+
data?: {
811+
managementToken: string;
812+
walletToken?: string;
813+
};
814+
}> {
815+
const { project, projectSecretKey } = props;
816+
817+
try {
818+
// Find the engineCloud service
819+
const engineCloudService = project.services.find(
820+
(service) => service.name === "engineCloud",
821+
);
822+
823+
if (!engineCloudService) {
824+
return {
825+
success: false,
826+
error: "No engineCloud service found on project",
827+
};
828+
}
829+
830+
const vaultClient = await initVaultClient();
831+
const hasEncryptedAdminKey = !!engineCloudService.encryptedAdminKey;
832+
833+
// Check if this is an ejected vault (no encrypted admin key stored)
834+
if (!hasEncryptedAdminKey) {
835+
// For ejected vaults, we only need to update the management token
836+
// User manages their own admin key, so we can't create wallet tokens
837+
838+
// We need the admin key from the user
839+
if (!projectSecretKey) {
840+
return {
841+
success: false,
842+
error: "Admin key required. Please enter your vault admin key.",
843+
};
844+
}
845+
846+
// For ejected vault, the "secret key" parameter is actually the admin key
847+
const managementTokenResult = await createManagementAccessToken({
848+
project,
849+
adminKey: projectSecretKey,
850+
vaultClient,
851+
});
852+
853+
if (!managementTokenResult.success) {
854+
return {
855+
success: false,
856+
error: `Failed to create management token: ${managementTokenResult.error}`,
857+
};
858+
}
859+
860+
// Update only the management token for ejected vaults
861+
// Keep everything else the same (no encrypted keys to update)
862+
await updateProjectClient(
863+
{
864+
projectId: project.id,
865+
teamId: project.teamId,
866+
},
867+
{
868+
services: [
869+
...project.services.filter(
870+
(service) => service.name !== "engineCloud",
871+
),
872+
{
873+
...engineCloudService,
874+
managementAccessToken: managementTokenResult.data.accessToken,
875+
},
876+
],
877+
},
878+
);
879+
880+
return {
881+
success: true,
882+
data: {
883+
managementToken: managementTokenResult.data.accessToken,
884+
},
885+
};
886+
}
887+
888+
// For non-ejected vaults (with encrypted admin key)
889+
if (!projectSecretKey) {
890+
return {
891+
success: false,
892+
error: "Project secret key is required to upgrade tokens",
893+
};
894+
}
895+
896+
// Verify the project secret key
897+
const projectSecretKeyHash = await hashSecretKey(projectSecretKey);
898+
if (!project.secretKeys.some((key) => key?.hash === projectSecretKeyHash)) {
899+
return {
900+
success: false,
901+
error: "Invalid project secret key",
902+
};
903+
}
904+
905+
// Decrypt the admin key (we know it exists from the hasEncryptedAdminKey check)
906+
const adminKey = await decrypt(
907+
engineCloudService.encryptedAdminKey as string,
908+
projectSecretKey,
909+
);
910+
911+
// Create new tokens with Solana permissions
912+
const [managementTokenResult, walletTokenResult] = await Promise.all([
913+
createManagementAccessToken({ project, adminKey, vaultClient }),
914+
createWalletAccessToken({ project, adminKey, vaultClient }),
915+
]);
916+
917+
if (!managementTokenResult.success) {
918+
return {
919+
success: false,
920+
error: `Failed to create management token: ${managementTokenResult.error}`,
921+
};
922+
}
923+
924+
if (!walletTokenResult.success) {
925+
return {
926+
success: false,
927+
error: `Failed to create wallet token: ${walletTokenResult.error}`,
928+
};
929+
}
930+
931+
const managementToken = managementTokenResult.data;
932+
const walletToken = walletTokenResult.data;
933+
934+
// Encrypt the new wallet token
935+
const [encryptedAdminKey, encryptedWalletAccessToken] = await Promise.all([
936+
encrypt(adminKey, projectSecretKey),
937+
encrypt(walletToken.accessToken, projectSecretKey),
938+
]);
939+
940+
// Update the project with new tokens
941+
await updateProjectClient(
942+
{
943+
projectId: project.id,
944+
teamId: project.teamId,
945+
},
946+
{
947+
services: [
948+
...project.services.filter(
949+
(service) => service.name !== "engineCloud",
950+
),
951+
{
952+
...engineCloudService,
953+
managementAccessToken: managementToken.accessToken,
954+
encryptedAdminKey,
955+
encryptedWalletAccessToken,
956+
},
957+
],
958+
},
959+
);
960+
961+
return {
962+
success: true,
963+
data: {
964+
managementToken: managementToken.accessToken,
965+
walletToken: walletToken.accessToken,
966+
},
967+
};
968+
} catch (error) {
969+
return {
970+
success: false,
971+
error:
972+
error instanceof Error
973+
? error.message
974+
: "Failed to upgrade access tokens",
975+
};
976+
}
977+
}
978+
798979
export function maskSecret(secret: string) {
799980
return `${secret.substring(0, 11)}...${secret.substring(secret.length - 5)}`;
800981
}

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/page.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import type { Wallet } from "./server-wallets/wallet-table/types";
1515
import { listSolanaAccounts } from "./solana-wallets/lib/vault.client";
1616
import type { SolanaWallet } from "./solana-wallets/wallet-table/types";
1717

18+
export const dynamic = "force-dynamic";
19+
1820
export default async function TransactionsAnalyticsPage(props: {
1921
params: Promise<{ team_slug: string; project_slug: string }>;
2022
searchParams: Promise<{

0 commit comments

Comments
 (0)