Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,68 +1,27 @@
"use server";

import { createVaultClient, listEoas } from "@thirdweb-dev/vault-sdk";
import { NEXT_PUBLIC_THIRDWEB_VAULT_URL } from "@/constants/public-envs";
import { listVaultServerWallets } from "@/actions/vault";
import type { ProjectWalletSummary } from "@/lib/server/project-wallet";

interface VaultWalletListItem {
id: string;
address: string;
metadata?: {
label?: string;
projectId?: string;
teamId?: string;
type?: string;
} | null;
}

export async function listProjectServerWallets(params: {
managementAccessToken: string;
teamId: string;
projectId: string;
pageSize?: number;
}): Promise<ProjectWalletSummary[]> {
const { managementAccessToken, projectId, pageSize = 100 } = params;

if (!managementAccessToken || !NEXT_PUBLIC_THIRDWEB_VAULT_URL) {
return [];
}
const { teamId, projectId, pageSize = 100 } = params;

try {
const vaultClient = await createVaultClient({
baseUrl: NEXT_PUBLIC_THIRDWEB_VAULT_URL,
const { wallets } = await listVaultServerWallets({
chainType: "evm",
pageSize,
project: { projectId, teamId },
});

const response = await listEoas({
client: vaultClient,
request: {
auth: {
accessToken: managementAccessToken,
},
options: {
page: 0,
// @ts-expect-error - Vault SDK expects snake_case pagination fields
page_size: pageSize,
},
},
});

if (!response.success || !response.data?.items) {
return [];
}

const items = response.data.items as VaultWalletListItem[];

return items
.filter((item) => {
return (
item.metadata?.projectId === projectId &&
(!item.metadata?.type || item.metadata.type === "server-wallet")
);
})
.map<ProjectWalletSummary>((item) => ({
id: item.id,
address: item.address,
label: item.metadata?.label ?? undefined,
}));
return wallets.map<ProjectWalletSummary>((wallet) => ({
address: wallet.address,
id: wallet.address,
label: wallet.label ?? undefined,
}));
} catch (error) {
console.error("Failed to list project server wallets", error);
return [];
Expand Down
2 changes: 1 addition & 1 deletion apps/dashboard/src/@/actions/proxies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { ANALYTICS_SERVICE_URL } from "@/constants/server-envs";
type ProxyActionParams = {
pathname: string;
searchParams?: Record<string, string | undefined>;
method: "GET" | "POST" | "PUT" | "DELETE";
method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
body?: string;
headers?: Record<string, string>;
parseAsText?: boolean;
Expand Down
256 changes: 256 additions & 0 deletions apps/dashboard/src/@/actions/vault.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
"use server";

import { apiServerProxy } from "@/actions/proxies";

type VaultChainType = "evm" | "solana";

type VaultServerWallet = {
address: string;
label: string | null;
chainType: VaultChainType;
createdAt: string;
updatedAt: string;
};

type VaultServerWalletPage = {
wallets: VaultServerWallet[];
page: number;
pageSize: number;
totalRecords: number;
projectWalletAddress: string | null;
};

type VaultAccessToken = {
id: string;
accessToken: string | null;
maskedAccessToken: string;
purpose: string | null;
createdAt: string;
updatedAt: string;
expiresAt: string;
};

type VaultAccessTokenPage = {
accessTokens: VaultAccessToken[];
page: number;
pageSize: number;
totalRecords: number;
revealed: boolean;
};

type RotateVaultServiceAccountResult = {
isManagedVault: boolean;
maskedAdminKey: string;
adminKey?: string;
walletAccessToken?: string;
};

/**
* Credentials the caller supplies to unlock a vault. A managed vault takes the
* project secret key, an ejected one takes the vault admin key.
*/
export type VaultCredentials = {
projectSecretKey?: string;
adminKey?: string;
};

type ProjectRef = {
teamId: string;
projectId: string;
};

function vaultPath(project: ProjectRef, path: string) {
return `/v1/teams/${project.teamId}/projects/${project.projectId}/vault${path}`;
}

/** API server errors are `{ data: null, error: { message, code, ... } }`. */
function errorMessage(body: string, fallback: string) {
try {
const parsed = JSON.parse(body) as { error?: { message?: string } };
if (parsed.error?.message) {
return parsed.error.message;
}
} catch {
// not JSON, fall through
}
return body || fallback;
}

export async function listVaultServerWallets(params: {
project: ProjectRef;
chainType: VaultChainType;
page?: number;
pageSize?: number;
}): Promise<VaultServerWalletPage> {
const res = await apiServerProxy<{ data: VaultServerWalletPage }>({
method: "GET",
pathname: vaultPath(params.project, "/wallets"),
searchParams: {
chainType: params.chainType,
page: params.page?.toString(),
pageSize: params.pageSize?.toString(),
},
});

if (!res.ok) {
throw new Error(errorMessage(res.error, "Failed to list server wallets"));
}

return res.data.data;
}

export async function createVaultServerWallet(params: {
project: ProjectRef;
chainType: VaultChainType;
label?: string;
setAsProjectWallet?: boolean;
}): Promise<{
wallet: VaultServerWallet;
smartAccountCached: boolean;
projectWalletAddress: string | null;
}> {
const res = await apiServerProxy<{
data: {
wallet: VaultServerWallet;
smartAccountCached: boolean;
projectWalletAddress: string | null;
};
}>({
body: JSON.stringify({
chainType: params.chainType,
label: params.label,
setAsProjectWallet: params.setAsProjectWallet,
}),
headers: { "Content-Type": "application/json" },
method: "POST",
pathname: vaultPath(params.project, "/wallets"),
});

if (!res.ok) {
throw new Error(errorMessage(res.error, "Failed to create server wallet"));
}

return res.data.data;
}

export async function setVaultProjectWallet(params: {
project: ProjectRef;
address: string;
}): Promise<{ projectWalletAddress: string }> {
const res = await apiServerProxy<{
data: { projectWalletAddress: string };
}>({
body: JSON.stringify({ address: params.address }),
headers: { "Content-Type": "application/json" },
method: "PATCH",
pathname: vaultPath(params.project, "/project-wallet"),
});

if (!res.ok) {
throw new Error(errorMessage(res.error, "Failed to update project wallet"));
}

return res.data.data;
}

export async function rotateVaultServiceAccount(params: {
project: ProjectRef;
projectSecretKey?: string;
}): Promise<RotateVaultServiceAccountResult> {
const res = await apiServerProxy<{
data: RotateVaultServiceAccountResult;
}>({
body: JSON.stringify({ projectSecretKey: params.projectSecretKey }),
headers: { "Content-Type": "application/json" },
method: "POST",
pathname: vaultPath(params.project, "/service-account/rotate"),
});

if (!res.ok) {
throw new Error(errorMessage(res.error, "Failed to rotate admin key"));
}

return res.data.data;
}

export async function listVaultAccessTokens(params: {
project: ProjectRef;
credentials?: VaultCredentials;
page?: number;
pageSize?: number;
}): Promise<VaultAccessTokenPage> {
const { credentials } = params;

const res = await apiServerProxy<{ data: VaultAccessTokenPage }>({
headers: {
...(credentials?.projectSecretKey
? { "x-vault-project-secret-key": credentials.projectSecretKey }
: {}),
...(credentials?.adminKey
? { "x-vault-admin-key": credentials.adminKey }
: {}),
},
method: "GET",
pathname: vaultPath(params.project, "/access-tokens"),
searchParams: {
page: params.page?.toString(),
pageSize: params.pageSize?.toString(),
},
});

if (!res.ok) {
throw new Error(errorMessage(res.error, "Failed to list access tokens"));
}

return res.data.data;
}

export async function createVaultAccessToken(params: {
project: ProjectRef;
credentials: VaultCredentials;
label?: string;
}): Promise<{ id: string; accessToken: string; purpose: string }> {
const res = await apiServerProxy<{
data: { id: string; accessToken: string; purpose: string };
}>({
body: JSON.stringify({
adminKey: params.credentials.adminKey,
label: params.label,
projectSecretKey: params.credentials.projectSecretKey,
}),
headers: { "Content-Type": "application/json" },
method: "POST",
pathname: vaultPath(params.project, "/access-tokens"),
});

if (!res.ok) {
throw new Error(errorMessage(res.error, "Failed to create access token"));
}

return res.data.data;
}

export async function revokeVaultAccessToken(params: {
project: ProjectRef;
credentials: VaultCredentials;
accessTokenId: string;
}): Promise<{ id: string }> {
const res = await apiServerProxy<{ data: { id: string } }>({
body: JSON.stringify({
adminKey: params.credentials.adminKey,
projectSecretKey: params.credentials.projectSecretKey,
}),
headers: { "Content-Type": "application/json" },
method: "DELETE",
pathname: vaultPath(
params.project,
`/access-tokens/${encodeURIComponent(params.accessTokenId)}`,
),
});

if (!res.ok) {
throw new Error(errorMessage(res.error, "Failed to revoke access token"));
}

return res.data.data;
}
Loading
Loading