diff --git a/apps/dashboard/src/app/(dashboard)/dashboard/contracts/deploy/page.tsx b/apps/dashboard/src/app/(dashboard)/dashboard/contracts/deploy/page.tsx
new file mode 100644
index 00000000000..289ef906956
--- /dev/null
+++ b/apps/dashboard/src/app/(dashboard)/dashboard/contracts/deploy/page.tsx
@@ -0,0 +1,19 @@
+import { redirect } from "next/navigation";
+import { getAuthTokenWalletAddress } from "../../../../api/lib/getAuthToken";
+import { DeployedContractsPage } from "../../../../team/[team_slug]/[project_slug]/contracts/_components/DeployedContractsPage";
+
+export default function Page() {
+ const accountAddress = getAuthTokenWalletAddress();
+ if (!accountAddress) {
+ return redirect(
+ `/login?next=${encodeURIComponent("/dashboard/contracts/deploy")}`,
+ );
+ }
+
+ return (
+
+ );
+}
diff --git a/apps/dashboard/src/app/(dashboard)/dashboard/contracts/layout.tsx b/apps/dashboard/src/app/(dashboard)/dashboard/contracts/layout.tsx
new file mode 100644
index 00000000000..27dcd951ac8
--- /dev/null
+++ b/apps/dashboard/src/app/(dashboard)/dashboard/contracts/layout.tsx
@@ -0,0 +1,13 @@
+import { BillingAlerts } from "../../../../components/settings/Account/Billing/alerts/Alert";
+import { ContractsSidebarLayout } from "../../../../core-ui/sidebar/contracts";
+
+export default function Layout(props: {
+ children: React.ReactNode;
+}) {
+ return (
+
+
+ {props.children}
+
+ );
+}
diff --git a/apps/dashboard/src/app/(dashboard)/layout.tsx b/apps/dashboard/src/app/(dashboard)/layout.tsx
index 3b08e193fc2..c89aefb25ac 100644
--- a/apps/dashboard/src/app/(dashboard)/layout.tsx
+++ b/apps/dashboard/src/app/(dashboard)/layout.tsx
@@ -7,7 +7,7 @@ export default function DashboardLayout(props: { children: React.ReactNode }) {
-
{props.children}
+
{props.children}
diff --git a/apps/dashboard/src/app/api/lib/getAuthToken.ts b/apps/dashboard/src/app/api/lib/getAuthToken.ts
index 36c4b4232a2..24f3174458d 100644
--- a/apps/dashboard/src/app/api/lib/getAuthToken.ts
+++ b/apps/dashboard/src/app/api/lib/getAuthToken.ts
@@ -10,3 +10,19 @@ export function getAuthToken() {
return token;
}
+
+export function getAuthTokenWalletAddress() {
+ const cookiesManager = cookies();
+ const activeAccount = cookiesManager.get(COOKIE_ACTIVE_ACCOUNT)?.value;
+ if (!activeAccount) {
+ return null;
+ }
+
+ const token = cookiesManager.get(COOKIE_PREFIX_TOKEN + activeAccount)?.value;
+
+ if (token) {
+ return activeAccount;
+ }
+
+ return null;
+}
diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/contracts/DeployedContractsPageHeader.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/contracts/DeployedContractsPageHeader.tsx
new file mode 100644
index 00000000000..7949da8aa89
--- /dev/null
+++ b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/contracts/DeployedContractsPageHeader.tsx
@@ -0,0 +1,49 @@
+"use client";
+
+import { DownloadIcon, PlusIcon } from "lucide-react";
+import Link from "next/link";
+import { useState } from "react";
+import { Button } from "../../../../../@/components/ui/button";
+import { ImportModal } from "../../../../../components/contract-components/import-contract/modal";
+
+export function DeployedContractsPageHeader() {
+ const [importModalOpen, setImportModalOpen] = useState(false);
+
+ return (
+
+
{
+ setImportModalOpen(false);
+ }}
+ />
+
+
+
+ Your contracts
+
+
+ The list of contract instances that you have deployed or imported
+ with thirdweb across all networks
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/contracts/_components/DeployedContractsPage.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/contracts/_components/DeployedContractsPage.tsx
new file mode 100644
index 00000000000..f4d559c9995
--- /dev/null
+++ b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/contracts/_components/DeployedContractsPage.tsx
@@ -0,0 +1,48 @@
+import { Spinner } from "@/components/ui/Spinner/Spinner";
+import { Suspense } from "react";
+import { ClientOnly } from "../../../../../../components/ClientOnly/ClientOnly";
+import { DeployedContractsPageHeader } from "../DeployedContractsPageHeader";
+import { DeployedContractsTable } from "./DeployedContractsTable";
+import { GetStartedWithContractsDeploy } from "./GetStartedWithContractsDeploy";
+import { getSortedDeployedContracts } from "./getSortedDeployedContracts";
+
+export function DeployedContractsPage(props: {
+ address: string;
+ className?: string;
+}) {
+ return (
+
+ );
+}
+
+async function DeployedContractsPageAsync(props: {
+ address: string;
+}) {
+ const deployedContracts = await getSortedDeployedContracts({
+ address: props.address,
+ });
+
+ if (deployedContracts.length === 0) {
+ return ;
+ }
+
+ return (
+ }>
+
+
+ );
+}
+
+function Loading() {
+ return (
+
+
+
+ );
+}
diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/contracts/_components/DeployedContractsTable.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/contracts/_components/DeployedContractsTable.tsx
new file mode 100644
index 00000000000..8e69d6220fc
--- /dev/null
+++ b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/contracts/_components/DeployedContractsTable.tsx
@@ -0,0 +1,18 @@
+"use client";
+import { useDashboardRouter } from "@/lib/DashboardRouter";
+import type { BasicContract } from "contract-ui/types/types";
+import { DeployedContracts } from "../../../../../../components/contract-components/tables/deployed-contracts";
+
+export function DeployedContractsTable(props: {
+ contracts: BasicContract[];
+}) {
+ const router = useDashboardRouter();
+ return (
+
+ );
+}
diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/contracts/_components/GetStartedWithContractsDeploy.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/contracts/_components/GetStartedWithContractsDeploy.tsx
index b16212281f9..29ee0c5474c 100644
--- a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/contracts/_components/GetStartedWithContractsDeploy.tsx
+++ b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/contracts/_components/GetStartedWithContractsDeploy.tsx
@@ -1,26 +1,15 @@
"use client";
import { TabButtons } from "@/components/ui/tabs";
import { useDashboardRouter } from "@/lib/DashboardRouter";
-import { CustomConnectWallet } from "@3rdweb-sdk/react/components/connect-wallet";
import Image from "next/image";
import { useMemo, useState } from "react";
-import { useActiveAccount } from "thirdweb/react";
import { ImportModal } from "../../../../../../components/contract-components/import-contract/modal";
import { StepsCard } from "../../../../../../components/dashboard/StepsCard";
import { useTrack } from "../../../../../../hooks/analytics/useTrack";
export function GetStartedWithContractsDeploy() {
- const address = useActiveAccount()?.address;
const steps = useMemo(
() => [
- {
- title: "Connect your wallet to get started",
- description:
- "In order to interact with your contracts you need to connect an EVM compatible wallet.",
- children: ,
- completed: !!address,
- },
-
{
title: "Build, deploy or import a contract",
description:
@@ -29,7 +18,7 @@ export function GetStartedWithContractsDeploy() {
completed: false, // because we only show this component if the user does not have any contracts
},
],
- [address],
+ [],
);
return (
@@ -139,7 +128,7 @@ const DeployOptions = () => {
{activeTabContent.title}
-
+
{activeTabContent.description}
diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/contracts/_components/getSortedDeployedContracts.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/contracts/_components/getSortedDeployedContracts.tsx
new file mode 100644
index 00000000000..f7395fd0ee0
--- /dev/null
+++ b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/contracts/_components/getSortedDeployedContracts.tsx
@@ -0,0 +1,42 @@
+import type { BasicContract } from "contract-ui/types/types";
+import { MULTICHAIN_REGISTRY_CONTRACT } from "../../../../../../constants/contracts";
+import { getAllMultichainRegistry } from "../../../../../../dashboard-extensions/common/read/getAllMultichainRegistry";
+import { fetchChain } from "../../../../../../utils/fetchChain";
+
+export async function getSortedDeployedContracts(params: {
+ address: string;
+}) {
+ const contracts = await getAllMultichainRegistry({
+ contract: MULTICHAIN_REGISTRY_CONTRACT,
+ address: params.address,
+ });
+
+ const chainIds = Array.from(new Set(contracts.map((c) => c.chainId)));
+ const chains = (
+ await Promise.allSettled(
+ chainIds.map((chainId) => fetchChain(chainId.toString())),
+ )
+ )
+ .filter((c) => c.status === "fulfilled")
+ .map((c) => c.value)
+ .filter((c) => c !== null);
+
+ const mainnetContracts: BasicContract[] = [];
+ const testnetContracts: BasicContract[] = [];
+
+ for (const contract of contracts) {
+ const chain = chains.find((chain) => contract.chainId === chain.chainId);
+ if (chain && chain.status !== "deprecated") {
+ if (chain.testnet) {
+ testnetContracts.push(contract);
+ } else {
+ mainnetContracts.push(contract);
+ }
+ }
+ }
+
+ mainnetContracts.sort((a, b) => a.chainId - b.chainId);
+ testnetContracts.sort((a, b) => a.chainId - b.chainId);
+
+ return [...mainnetContracts, ...testnetContracts];
+}
diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/contracts/layout.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/contracts/layout.tsx
index 38c1be3122f..500764ffd23 100644
--- a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/contracts/layout.tsx
+++ b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/contracts/layout.tsx
@@ -1,5 +1,5 @@
export default function Layout(props: {
children: React.ReactNode;
}) {
- return {props.children}
;
+ return {props.children}
;
}
diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/contracts/page.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/contracts/page.tsx
index 4389c0b266b..c305fbeae6c 100644
--- a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/contracts/page.tsx
+++ b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/contracts/page.tsx
@@ -1,31 +1,23 @@
-"use client";
-import { Spinner } from "@/components/ui/Spinner/Spinner";
-import { useAllContractList } from "@3rdweb-sdk/react/hooks/useRegistry";
-import { useActiveAccount } from "thirdweb/react";
-import { DeployedContracts } from "../../../../../components/contract-components/tables/deployed-contracts";
-import { GetStartedWithContractsDeploy } from "./_components/GetStartedWithContractsDeploy";
+import { redirect } from "next/navigation";
+import { getAuthTokenWalletAddress } from "../../../../api/lib/getAuthToken";
+import { DeployedContractsPage } from "./_components/DeployedContractsPage";
-export default function Page() {
- const address = useActiveAccount()?.address;
- const deployedContracts = useAllContractList(address);
- const hasContracts =
- deployedContracts.data && deployedContracts.data?.length > 0;
+export default function Page(props: {
+ params: { team_slug: string; project_slug: string };
+}) {
+ const { team_slug, project_slug } = props.params;
+ const accountAddress = getAuthTokenWalletAddress();
- if (deployedContracts.isPending) {
- return (
-
-
-
+ if (!accountAddress) {
+ return redirect(
+ `/login?next=${encodeURIComponent(`/team/${team_slug}/${project_slug}/contracts`)}`,
);
}
return (
-
- {!hasContracts ? (
-
- ) : (
-
- )}
-
+
);
}
diff --git a/apps/dashboard/src/components/contract-components/tables/deployed-contracts.tsx b/apps/dashboard/src/components/contract-components/tables/deployed-contracts.tsx
index 5cdb3d9c95a..331f94760e8 100644
--- a/apps/dashboard/src/components/contract-components/tables/deployed-contracts.tsx
+++ b/apps/dashboard/src/components/contract-components/tables/deployed-contracts.tsx
@@ -9,6 +9,7 @@ import {
DropdownMenuContent,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
+import { SkeletonContainer } from "@/components/ui/skeleton";
import {
Table,
TableBody,
@@ -26,13 +27,7 @@ import {
import { ChainIcon } from "components/icons/ChainIcon";
import { NetworkSelectDropdown } from "components/selects/NetworkSelectDropdown";
import type { BasicContract } from "contract-ui/types/types";
-import {
- DownloadIcon,
- EllipsisVerticalIcon,
- PlusIcon,
- XIcon,
-} from "lucide-react";
-import Link from "next/link";
+import { EllipsisVerticalIcon, XIcon } from "lucide-react";
import { memo, useEffect, useMemo, useState } from "react";
import {
type Column,
@@ -44,80 +39,42 @@ import {
} from "react-table";
import { useAllChainsData } from "../../../hooks/chains/allChains";
import { useChainSlug } from "../../../hooks/chains/chainSlug";
-import { ImportModal } from "../import-contract/modal";
import { AsyncContractNameCell, AsyncContractTypeCell } from "./cells";
import { ShowMoreButton } from "./show-more-button";
interface DeployedContractsProps {
- noHeader?: boolean;
- contractListQuery: ReturnType;
+ contractList: ReturnType["data"];
+ isPending: boolean;
limit?: number;
+ onContractRemoved?: () => void;
}
export const DeployedContracts: React.FC = ({
- noHeader,
- contractListQuery,
+ contractList,
limit = 10,
+ isPending,
+ onContractRemoved,
}) => {
- const [importModalOpen, setImportModalOpen] = useState(false);
-
const chainIdsWithDeployments = useMemo(() => {
const set = new Set();
// biome-ignore lint/complexity/noForEach: FIXME
- contractListQuery.data.forEach((contract) => {
+ contractList.forEach((contract) => {
set.add(contract.chainId);
});
return [...set];
- }, [contractListQuery.data]);
+ }, [contractList]);
return (
- {!noHeader && (
- <>
-
{
- setImportModalOpen(false);
- }}
- />
-
-
-
- Your contracts
-
-
- The list of contract instances that you have deployed or
- imported with thirdweb across all networks
-
-
-
-
-
-
-
- >
- )}
-
- {contractListQuery.data.length === 0 && contractListQuery.isFetched && (
+ {contractList.length === 0 && !isPending && (
No contracts found
@@ -129,11 +86,13 @@ export const DeployedContracts: React.FC = ({
type RemoveFromDashboardButtonProps = {
chainId: number;
contractAddress: string;
+ onContractRemoved?: () => void;
};
const RemoveFromDashboardButton: React.FC = ({
chainId,
contractAddress,
+ onContractRemoved,
}) => {
const mutation = useRemoveContractMutation();
@@ -142,7 +101,14 @@ const RemoveFromDashboardButton: React.FC = ({
variant="ghost"
onClick={(e) => {
e.stopPropagation();
- mutation.mutate({ chainId, contractAddress });
+ mutation.mutateAsync(
+ { chainId, contractAddress },
+ {
+ onSuccess: () => {
+ onContractRemoved?.();
+ },
+ },
+ );
}}
disabled={mutation.isPending}
className="!bg-background hover:!bg-accent gap-2"
@@ -188,6 +154,7 @@ interface ContractTableProps {
limit: number;
chainIdsWithDeployments: number[];
loading: boolean;
+ onContractRemoved?: () => void;
}
const ContractTable: React.FC = ({
@@ -196,6 +163,7 @@ const ContractTable: React.FC = ({
limit,
chainIdsWithDeployments,
loading,
+ onContractRemoved,
}) => {
const { idToChain } = useAllChainsData();
@@ -236,9 +204,14 @@ const ContractTable: React.FC = ({
return (
-
- {cleanedChainName}
-
+
{
+ return {v}
;
+ }}
+ />
+
{data?.testnet && (
Testnet
@@ -285,6 +258,7 @@ const ContractTable: React.FC = ({
@@ -292,7 +266,7 @@ const ContractTable: React.FC = ({
},
},
],
- [chainIdsWithDeployments, idToChain],
+ [chainIdsWithDeployments, idToChain, onContractRemoved],
);
const defaultColumn = useMemo(
@@ -395,10 +369,12 @@ const ContractTable: React.FC = ({
const ContractTableRow = memo(({ row }: { row: Row }) => {
const chainSlug = useChainSlug(row.original.chainId);
const router = useDashboardRouter();
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const { key, ...rowProps } = row.getRowProps();
return (
{
- const address = useActiveAccount()?.address;
- const deployedContracts = useAllContractList(address);
-
- if (deployedContracts.isPending) {
- return (
-
-
-
- );
- }
-
- const hasContracts =
- deployedContracts.data && deployedContracts.data?.length > 0;
-
- if (!hasContracts) {
- return ;
- }
-
- return ;
-};
-
-Contracts.getLayout = (page, props) => (
-
- {page}
-
-);
-Contracts.pageId = PageId.Contracts;
-
-export default Contracts;
diff --git a/apps/dashboard/src/pages/profile/[profileAddress].tsx b/apps/dashboard/src/pages/profile/[profileAddress].tsx
index 14f43a956c9..a2408cfaf50 100644
--- a/apps/dashboard/src/pages/profile/[profileAddress].tsx
+++ b/apps/dashboard/src/pages/profile/[profileAddress].tsx
@@ -177,8 +177,8 @@ const UserPage: ThirdwebNextPage = (props: UserPageProps) => {
{ens.data?.address && (
)}