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
@@ -0,0 +1,70 @@
"use server";

import { createVaultClient, listEoas } from "@thirdweb-dev/vault-sdk";
import { NEXT_PUBLIC_THIRDWEB_VAULT_URL } from "@/constants/public-envs";
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;
projectId: string;
pageSize?: number;
}): Promise<ProjectWalletSummary[]> {
const { managementAccessToken, projectId, pageSize = 100 } = params;

if (!managementAccessToken || !NEXT_PUBLIC_THIRDWEB_VAULT_URL) {
return [];
}

try {
const vaultClient = await createVaultClient({
baseUrl: NEXT_PUBLIC_THIRDWEB_VAULT_URL,
});

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,
}));
} catch (error) {
console.error("Failed to list project server wallets", error);
return [];
}
}
74 changes: 74 additions & 0 deletions apps/dashboard/src/@/actions/project-wallet/send-tokens.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
"use server";

import { configure, sendTokens } from "@thirdweb-dev/api";
import { THIRDWEB_API_HOST } from "@/constants/urls";

configure({
override: {
baseUrl: THIRDWEB_API_HOST,
},
});

export async function sendProjectWalletTokens(options: {
walletAddress: string;
recipientAddress: string;
chainId: number;
quantityWei: string;
publishableKey: string;
teamId: string;
tokenAddress?: string;
secretKey: string;
vaultAccessToken?: string;
}) {
const {
walletAddress,
recipientAddress,
chainId,
quantityWei,
publishableKey,
teamId,
tokenAddress,
secretKey,
vaultAccessToken,
} = options;

if (!secretKey) {
return {
error: "A project secret key is required to send funds.",
ok: false,
} as const;
}

const response = await sendTokens({
body: {
chainId,
from: walletAddress,
recipients: [
{
address: recipientAddress,
quantity: quantityWei,
},
],
...(tokenAddress ? { tokenAddress } : {}),
},
headers: {
"Content-Type": "application/json",
"x-client-id": publishableKey,
"x-secret-key": secretKey,
"x-team-id": teamId,
...(vaultAccessToken ? { "x-vault-access-token": vaultAccessToken } : {}),
},
});

if (response.error || !response.data) {
return {
error: response.error || "Failed to submit transfer request.",
ok: false,
} as const;
}

return {
ok: true,
transactionIds: response.data.result?.transactionIds ?? [],
} as const;
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Link from "next/link";
import { Button } from "@/components/ui/button";
import { useLocalStorage } from "@/hooks/useLocalStorage";

// biome-ignore lint/correctness/noUnusedVariables: banner is toggled on-demand via API content changes
function AnnouncementBannerUI(props: {
href: string;
label: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ const CreateProjectDialog = (props: CreateProjectDialogProps) => {
<CreateProjectDialogUI
createProject={async (params) => {
const res = await createProjectClient(props.teamId, params);
await createVaultAccountAndAccessToken({
const vaultTokens = await createVaultAccountAndAccessToken({
project: res.project,
projectSecretKey: res.secret,
}).catch((error) => {
Expand All @@ -74,6 +74,13 @@ 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,
Expand Down
21 changes: 21 additions & 0 deletions apps/dashboard/src/@/lib/project-wallet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
const PROJECT_WALLET_LABEL_SUFFIX = " Wallet";
const PROJECT_WALLET_LABEL_MAX_LENGTH = 32;

/**
* Builds the default label for a project's primary server wallet.
* Ensures a stable naming convention so the wallet can be identified across flows.
*/
export function getProjectWalletLabel(projectName: string | undefined) {
const baseName = projectName?.trim() || "Project";
const maxBaseLength = Math.max(
1,
PROJECT_WALLET_LABEL_MAX_LENGTH - PROJECT_WALLET_LABEL_SUFFIX.length,
);

const normalizedBase =
baseName.length > maxBaseLength
? baseName.slice(0, maxBaseLength).trimEnd()
: baseName;

return `${normalizedBase}${PROJECT_WALLET_LABEL_SUFFIX}`;
}
Comment on lines +8 to +21
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Add explicit return type annotation.

The function lacks an explicit return type. Per coding guidelines, TypeScript functions should have explicit return types.

Apply this diff:

-export function getProjectWalletLabel(projectName: string | undefined) {
+export function getProjectWalletLabel(projectName: string | undefined): string {
   const baseName = projectName?.trim() || "Project";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function getProjectWalletLabel(projectName: string | undefined) {
const baseName = projectName?.trim() || "Project";
const maxBaseLength = Math.max(
1,
PROJECT_WALLET_LABEL_MAX_LENGTH - PROJECT_WALLET_LABEL_SUFFIX.length,
);
const normalizedBase =
baseName.length > maxBaseLength
? baseName.slice(0, maxBaseLength).trimEnd()
: baseName;
return `${normalizedBase}${PROJECT_WALLET_LABEL_SUFFIX}`;
}
export function getProjectWalletLabel(projectName: string | undefined): string {
const baseName = projectName?.trim() || "Project";
const maxBaseLength = Math.max(
1,
PROJECT_WALLET_LABEL_MAX_LENGTH - PROJECT_WALLET_LABEL_SUFFIX.length,
);
const normalizedBase =
baseName.length > maxBaseLength
? baseName.slice(0, maxBaseLength).trimEnd()
: baseName;
return `${normalizedBase}${PROJECT_WALLET_LABEL_SUFFIX}`;
}
🤖 Prompt for AI Agents
In apps/dashboard/src/@/lib/project-wallet.ts around lines 8 to 21, the function
getProjectWalletLabel is missing an explicit return type; update the function
signature to declare the return type explicitly (e.g., add ": string" after the
parameter list), keep the existing implementation unchanged, and run TypeScript
type-checking to ensure no further signature issues.

99 changes: 99 additions & 0 deletions apps/dashboard/src/@/lib/server/project-wallet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import "server-only";

import { createVaultClient, listEoas } from "@thirdweb-dev/vault-sdk";
import type { Project } from "@/api/project/projects";
import { NEXT_PUBLIC_THIRDWEB_VAULT_URL } from "@/constants/public-envs";
import { getProjectWalletLabel } from "@/lib/project-wallet";

export type ProjectWalletSummary = {
id: string;
address: string;
label?: string;
};

type VaultWalletListItem = {
id: string;
address: string;
metadata?: {
label?: string;
projectId?: string;
teamId?: string;
type?: string;
};
};

export async function getProjectWallet(
project: Project,
): Promise<ProjectWalletSummary | undefined> {
const engineCloudService = project.services.find(
(service) => service.name === "engineCloud",
);

const managementAccessToken =
engineCloudService?.managementAccessToken || undefined;
const projectWalletAddress = (
engineCloudService as { projectWalletAddress?: string } | undefined
)?.projectWalletAddress;

if (
!managementAccessToken ||
!NEXT_PUBLIC_THIRDWEB_VAULT_URL ||
!projectWalletAddress
) {
return undefined;
}

try {
const vaultClient = await createVaultClient({
baseUrl: NEXT_PUBLIC_THIRDWEB_VAULT_URL,
});

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

if (!response.success || !response.data) {
return undefined;
}

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

if (!items?.length) {
return undefined;
}

const defaultLabel = getProjectWalletLabel(project.name);

const serverWallets = items.filter(
(item) => item.metadata?.projectId === project.id,
);

const defaultWallet = serverWallets.find(
(item) =>
item.address.toLowerCase() === projectWalletAddress.toLowerCase(),
);

if (!defaultWallet) {
return undefined;
}

return {
id: defaultWallet.id,
address: defaultWallet.address,
label: defaultWallet.metadata?.label ?? defaultLabel,
};
} catch (error) {
console.error("Failed to load project wallet", error);
return undefined;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export function CreateProjectFormOnboarding(props: {
<CreateProjectForm
createProject={async (params) => {
const res = await createProjectClient(props.teamId, params);
await createVaultAccountAndAccessToken({
const vaultTokens = await createVaultAccountAndAccessToken({
project: res.project,
projectSecretKey: res.secret,
}).catch((error) => {
Expand All @@ -65,6 +65,16 @@ 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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,6 @@ export const Default: Story = {
],
},
teamSlug: "bar",
managementAccessToken: undefined,
},
};
Loading
Loading