Compare commits
46
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
89e6af6baf | ||
|
|
b345cc115e | ||
|
|
3d3c6f3d45 | ||
|
|
ff3a79c884 | ||
|
|
16482cfa6a | ||
|
|
e8a25d70b9 | ||
|
|
44a7460c83 | ||
|
|
0731831169 | ||
|
|
91750eddc6 | ||
|
|
2fc083f1f2 | ||
|
|
75ae536e61 | ||
|
|
41e59a9c20 | ||
|
|
fd97406fa2 | ||
|
|
c336de3bfb | ||
|
|
c420099dc3 | ||
|
|
dcb412fc27 | ||
|
|
4705a599a2 | ||
|
|
f7331f15a2 | ||
|
|
a5d6e769db | ||
|
|
97b8926024 | ||
|
|
655df037dd | ||
|
|
cbde32e974 | ||
|
|
8ed5ef3f81 | ||
|
|
e14a1349e1 | ||
|
|
acd8b9e535 | ||
|
|
52d451ff70 | ||
|
|
c03616666b | ||
|
|
bb7a8988ef | ||
|
|
8d0ffedd41 | ||
|
|
5edfb81ec5 | ||
|
|
f115b4d2db | ||
|
|
0e4390b2c7 | ||
|
|
aec8d88af2 | ||
|
|
80207ae240 | ||
|
|
ec7bc2bb1e | ||
|
|
12475027d1 | ||
|
|
9479d252b4 | ||
|
|
5139857db9 | ||
|
|
ad4341a7ad | ||
|
|
ebd58f49b6 | ||
|
|
3862bc7a94 | ||
|
|
5277eddb70 | ||
|
|
6c180b5c8a | ||
|
|
3489ecec98 | ||
|
|
f9b34ff2f1 | ||
|
|
f3abea3276 |
@@ -27,10 +27,7 @@ module.exports = [
|
||||
// -- engine
|
||||
"/engine",
|
||||
// -- solutions pages --
|
||||
"/solutions/gaming",
|
||||
"/solutions/consumer-apps",
|
||||
"/solutions/defi",
|
||||
"/solutions/ecosystem",
|
||||
"/solutions/:solution_slug",
|
||||
// -- campaigns --
|
||||
// "/unlimited-wallets", -- OFF for now
|
||||
// -- TPP --
|
||||
|
||||
@@ -40,8 +40,8 @@
|
||||
"@radix-ui/react-popover": "^1.1.10",
|
||||
"@radix-ui/react-progress": "^1.1.4",
|
||||
"@radix-ui/react-radio-group": "^1.3.3",
|
||||
"@radix-ui/react-select": "^2.2.2",
|
||||
"@radix-ui/react-separator": "^1.1.4",
|
||||
"@radix-ui/react-select": "^2.2.5",
|
||||
"@radix-ui/react-separator": "^1.1.7",
|
||||
"@radix-ui/react-slot": "^1.2.0",
|
||||
"@radix-ui/react-switch": "^1.2.2",
|
||||
"@radix-ui/react-tooltip": "1.2.3",
|
||||
@@ -77,7 +77,7 @@
|
||||
"nextjs-toploader": "^1.6.12",
|
||||
"nuqs": "^2.4.3",
|
||||
"p-limit": "^6.2.0",
|
||||
"papaparse": "^5.5.2",
|
||||
"papaparse": "^5.5.3",
|
||||
"pluralize": "^8.0.0",
|
||||
"posthog-js": "1.67.1",
|
||||
"prettier": "3.5.3",
|
||||
@@ -89,7 +89,7 @@
|
||||
"react-dropzone": "^14.3.8",
|
||||
"react-error-boundary": "^5.0.0",
|
||||
"react-hook-form": "7.55.0",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-markdown": "10.1.0",
|
||||
"react-table": "^7.8.0",
|
||||
"recharts": "2.15.3",
|
||||
"remark-gfm": "4.0.1",
|
||||
@@ -105,7 +105,7 @@
|
||||
"thirdweb": "workspace:*",
|
||||
"tiny-invariant": "^1.3.3",
|
||||
"use-debounce": "^10.0.4",
|
||||
"zod": "3.24.3"
|
||||
"zod": "3.25.24"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@chakra-ui/cli": "^2.4.1",
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
"use server";
|
||||
|
||||
import { getAuthToken } from "app/(app)/api/lib/getAuthToken";
|
||||
import { THIRDWEB_INSIGHT_API_DOMAIN } from "constants/urls";
|
||||
|
||||
export interface WebhookResponse {
|
||||
id: string;
|
||||
name: string;
|
||||
team_id: string;
|
||||
project_id: string;
|
||||
webhook_url: string;
|
||||
webhook_secret: string;
|
||||
filters: WebhookFilters;
|
||||
suspended_at: string | null;
|
||||
suspended_reason: string | null;
|
||||
disabled: boolean;
|
||||
created_at: string;
|
||||
updated_at: string | null;
|
||||
}
|
||||
|
||||
export interface WebhookFilters {
|
||||
"v1.events"?: {
|
||||
chain_ids?: string[];
|
||||
addresses?: string[];
|
||||
signatures?: Array<{
|
||||
sig_hash: string;
|
||||
abi?: string;
|
||||
params?: Record<string, unknown>;
|
||||
}>;
|
||||
};
|
||||
"v1.transactions"?: {
|
||||
chain_ids?: string[];
|
||||
from_addresses?: string[];
|
||||
to_addresses?: string[];
|
||||
signatures?: Array<{
|
||||
sig_hash: string;
|
||||
abi?: string;
|
||||
params?: Record<string, unknown>;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
|
||||
interface CreateWebhookPayload {
|
||||
name: string;
|
||||
webhook_url: string;
|
||||
filters: WebhookFilters;
|
||||
}
|
||||
|
||||
interface WebhooksListResponse {
|
||||
data: WebhookResponse[];
|
||||
error?: string;
|
||||
}
|
||||
|
||||
interface WebhookSingleResponse {
|
||||
data: WebhookResponse | null;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
interface TestWebhookPayload {
|
||||
webhook_url: string;
|
||||
type?: "event" | "transaction";
|
||||
}
|
||||
|
||||
interface TestWebhookResponse {
|
||||
success: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export async function createWebhook(
|
||||
payload: CreateWebhookPayload,
|
||||
clientId: string,
|
||||
): Promise<WebhookSingleResponse> {
|
||||
try {
|
||||
const authToken = await getAuthToken();
|
||||
const response = await fetch(`${THIRDWEB_INSIGHT_API_DOMAIN}/v1/webhooks`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-client-id": clientId,
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
return {
|
||||
data: null,
|
||||
error: `Failed to create webhook: ${errorText}`,
|
||||
};
|
||||
}
|
||||
|
||||
return (await response.json()) as WebhookSingleResponse;
|
||||
} catch (error) {
|
||||
return {
|
||||
data: null,
|
||||
error: `Network or parsing error: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function getWebhooks(
|
||||
clientId: string,
|
||||
): Promise<WebhooksListResponse> {
|
||||
try {
|
||||
const authToken = await getAuthToken();
|
||||
const response = await fetch(`${THIRDWEB_INSIGHT_API_DOMAIN}/v1/webhooks`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"x-client-id": clientId,
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
return {
|
||||
data: [],
|
||||
error: `Failed to get webhooks: ${errorText}`,
|
||||
};
|
||||
}
|
||||
|
||||
return (await response.json()) as WebhooksListResponse;
|
||||
} catch (error) {
|
||||
return {
|
||||
data: [],
|
||||
error: `Network or parsing error: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteWebhook(
|
||||
webhookId: string,
|
||||
clientId: string,
|
||||
): Promise<WebhookSingleResponse> {
|
||||
try {
|
||||
const authToken = await getAuthToken();
|
||||
const response = await fetch(
|
||||
`${THIRDWEB_INSIGHT_API_DOMAIN}/v1/webhooks/${encodeURIComponent(webhookId)}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"x-client-id": clientId,
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
return {
|
||||
data: null,
|
||||
error: `Failed to delete webhook: ${errorText}`,
|
||||
};
|
||||
}
|
||||
|
||||
return (await response.json()) as WebhookSingleResponse;
|
||||
} catch (error) {
|
||||
return {
|
||||
data: null,
|
||||
error: `Network or parsing error: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function testWebhook(
|
||||
payload: TestWebhookPayload,
|
||||
clientId: string,
|
||||
): Promise<TestWebhookResponse> {
|
||||
try {
|
||||
const authToken = await getAuthToken();
|
||||
const response = await fetch(
|
||||
`${THIRDWEB_INSIGHT_API_DOMAIN}/v1/webhooks/test`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-client-id": clientId,
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
return {
|
||||
success: false,
|
||||
error: `Failed to test webhook: ${errorText}`,
|
||||
};
|
||||
}
|
||||
|
||||
return (await response.json()) as TestWebhookResponse;
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Network or parsing error: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
"use server";
|
||||
import { DASHBOARD_THIRDWEB_SECRET_KEY } from "@/constants/server-envs";
|
||||
import type { ProjectResponse } from "@thirdweb-dev/service-utils";
|
||||
import { getAuthToken } from "app/(app)/api/lib/getAuthToken";
|
||||
import { UB_BASE_URL } from "./constants";
|
||||
|
||||
export type TokenMetadata = {
|
||||
@@ -37,3 +39,33 @@ export async function getUniversalBridgeTokens(props: {
|
||||
const json = await res.json();
|
||||
return json.data as Array<TokenMetadata>;
|
||||
}
|
||||
|
||||
export async function addUniversalBridgeTokenRoute(props: {
|
||||
chainId: number;
|
||||
tokenAddress: string;
|
||||
project: ProjectResponse;
|
||||
}) {
|
||||
const authToken = await getAuthToken();
|
||||
const url = new URL(`${UB_BASE_URL}/v1/tokens`);
|
||||
|
||||
const res = await fetch(url.toString(), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
"x-client-id": props.project.publishableKey,
|
||||
} as Record<string, string>,
|
||||
body: JSON.stringify({
|
||||
chainId: props.chainId,
|
||||
tokenAddress: props.tokenAddress,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
throw new Error(text);
|
||||
}
|
||||
|
||||
const json = await res.json();
|
||||
return json.data as Array<TokenMetadata>;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type React from "react";
|
||||
|
||||
export function RouteDiscoveryCard(
|
||||
props: React.PropsWithChildren<{
|
||||
bottomText: React.ReactNode;
|
||||
header?: {
|
||||
description: string | undefined;
|
||||
title: string;
|
||||
};
|
||||
errorText: string | undefined;
|
||||
noPermissionText: string | undefined;
|
||||
saveButton?: {
|
||||
onClick?: () => void;
|
||||
disabled: boolean;
|
||||
isPending: boolean;
|
||||
type?: "submit";
|
||||
variant?:
|
||||
| "ghost"
|
||||
| "default"
|
||||
| "primary"
|
||||
| "destructive"
|
||||
| "outline"
|
||||
| "secondary";
|
||||
className?: string;
|
||||
label?: string;
|
||||
};
|
||||
}>,
|
||||
) {
|
||||
return (
|
||||
<div className="relative rounded-lg border border-border bg-card">
|
||||
<div
|
||||
className={cn(
|
||||
"relative border-border border-b px-4 py-6 lg:px-6",
|
||||
props.noPermissionText && "cursor-not-allowed",
|
||||
)}
|
||||
>
|
||||
{props.header && (
|
||||
<>
|
||||
<h3 className="font-semibold text-xl tracking-tight">
|
||||
{props.header.title}
|
||||
</h3>
|
||||
{props.header.description && (
|
||||
<p className="mt-1.5 mb-4 text-foreground text-sm">
|
||||
{props.header.description}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{props.children}
|
||||
</div>
|
||||
|
||||
<div className="flex min-h-[60px] items-center justify-between gap-2 px-4 py-3 lg:px-6">
|
||||
{props.noPermissionText ? (
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{props.noPermissionText}
|
||||
</p>
|
||||
) : props.errorText ? (
|
||||
<p className="text-destructive-text text-sm">{props.errorText}</p>
|
||||
) : (
|
||||
<p className="text-muted-foreground text-sm">{props.bottomText}</p>
|
||||
)}
|
||||
|
||||
{props.saveButton && !props.noPermissionText && (
|
||||
<Button
|
||||
size="sm"
|
||||
className={cn("gap-2", props.saveButton.className)}
|
||||
onClick={props.saveButton.onClick}
|
||||
disabled={props.saveButton.disabled || props.saveButton.isPending}
|
||||
variant={props.saveButton.variant || "outline"}
|
||||
type={props.saveButton.type}
|
||||
>
|
||||
{props.saveButton.isPending && <Spinner className="size-3" />}
|
||||
{props.saveButton.label ||
|
||||
(props.saveButton.isPending ? "Submit" : "Submit Token")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
-29
@@ -1,29 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { getSDKTheme } from "app/(app)/components/sdk-component-theme";
|
||||
import { useV5DashboardChain } from "lib/v5-adapter";
|
||||
import { useTheme } from "next-themes";
|
||||
import type { ThirdwebClient } from "thirdweb";
|
||||
import { PayEmbed } from "thirdweb/react";
|
||||
|
||||
export function UniversalBridgeEmbed({
|
||||
chainId,
|
||||
client,
|
||||
}: { chainId?: number; client: ThirdwebClient }) {
|
||||
const { theme } = useTheme();
|
||||
const chain = useV5DashboardChain(chainId || 1);
|
||||
|
||||
return (
|
||||
<PayEmbed
|
||||
client={client}
|
||||
payOptions={{
|
||||
mode: "fund_wallet",
|
||||
prefillBuy: {
|
||||
chain,
|
||||
amount: "0.01",
|
||||
},
|
||||
}}
|
||||
theme={getSDKTheme(theme === "light" ? "light" : "dark")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
+8
-1
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import type { ProjectMeta } from "../../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { ErrorPage, LoadingPage } from "../../_components/page-skeletons";
|
||||
import { RedirectToContractOverview } from "../../_components/redirect-contract-overview.client";
|
||||
import { useContractPageMetadata } from "../../_hooks/useContractPageMetadata";
|
||||
@@ -9,6 +10,7 @@ import { ContractDirectListingsPage } from "./ContractDirectListingsPage";
|
||||
export function ContractDirectListingsPageClient(props: {
|
||||
contract: ThirdwebContract;
|
||||
isLoggedIn: boolean;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
}) {
|
||||
const metadataQuery = useContractPageMetadata(props.contract);
|
||||
|
||||
@@ -21,7 +23,12 @@ export function ContractDirectListingsPageClient(props: {
|
||||
}
|
||||
|
||||
if (!metadataQuery.data.isDirectListingSupported) {
|
||||
return <RedirectToContractOverview contract={props.contract} />;
|
||||
return (
|
||||
<RedirectToContractOverview
|
||||
contract={props.contract}
|
||||
projectMeta={props.projectMeta}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
+8
-35
@@ -1,45 +1,18 @@
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { getRawAccount } from "../../../../../../account/settings/getAccount";
|
||||
import { getContractPageParamsInfo } from "../../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../../_utils/getContractPageMetadata";
|
||||
import { ContractDirectListingsPage } from "./ContractDirectListingsPage";
|
||||
import { ContractDirectListingsPageClient } from "./ContractDirectListingsPage.client";
|
||||
import type { PublicContractPageParams } from "../../types";
|
||||
import { SharedDirectListingsPage } from "./shared-direct-listings-page";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: Promise<{
|
||||
contractAddress: string;
|
||||
chain_id: string;
|
||||
}>;
|
||||
params: Promise<PublicContractPageParams>;
|
||||
}) {
|
||||
const params = await props.params;
|
||||
const account = await getRawAccount();
|
||||
const info = await getContractPageParamsInfo(params);
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
if (info.isLocalhostChain) {
|
||||
return (
|
||||
<ContractDirectListingsPageClient
|
||||
contract={info.clientContract}
|
||||
isLoggedIn={!!account}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const { isDirectListingSupported, isInsightSupported } =
|
||||
await getContractPageMetadata(info.serverContract);
|
||||
|
||||
if (!isDirectListingSupported) {
|
||||
redirect(`/${params.chain_id}/${params.contractAddress}`);
|
||||
}
|
||||
const [params, account] = await Promise.all([props.params, getRawAccount()]);
|
||||
|
||||
return (
|
||||
<ContractDirectListingsPage
|
||||
contract={info.clientContract}
|
||||
<SharedDirectListingsPage
|
||||
contractAddress={params.contractAddress}
|
||||
chainIdOrSlug={params.chain_id}
|
||||
projectMeta={undefined}
|
||||
isLoggedIn={!!account}
|
||||
isInsightSupported={isInsightSupported}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import type { ProjectMeta } from "../../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { redirectToContractLandingPage } from "../../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/utils";
|
||||
import { getContractPageParamsInfo } from "../../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../../_utils/getContractPageMetadata";
|
||||
import { ContractDirectListingsPage } from "./ContractDirectListingsPage";
|
||||
import { ContractDirectListingsPageClient } from "./ContractDirectListingsPage.client";
|
||||
|
||||
export async function SharedDirectListingsPage(props: {
|
||||
contractAddress: string;
|
||||
chainIdOrSlug: string;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
isLoggedIn: boolean;
|
||||
}) {
|
||||
const info = await getContractPageParamsInfo({
|
||||
contractAddress: props.contractAddress,
|
||||
chainIdOrSlug: props.chainIdOrSlug,
|
||||
teamId: props.projectMeta?.teamId,
|
||||
});
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
if (info.isLocalhostChain) {
|
||||
return (
|
||||
<ContractDirectListingsPageClient
|
||||
contract={info.clientContract}
|
||||
isLoggedIn={props.isLoggedIn}
|
||||
projectMeta={props.projectMeta}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const { isDirectListingSupported, isInsightSupported } =
|
||||
await getContractPageMetadata(info.serverContract);
|
||||
|
||||
if (!isDirectListingSupported) {
|
||||
redirectToContractLandingPage({
|
||||
chainIdOrSlug: props.chainIdOrSlug,
|
||||
contractAddress: props.contractAddress,
|
||||
projectMeta: props.projectMeta,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<ContractDirectListingsPage
|
||||
contract={info.clientContract}
|
||||
isLoggedIn={props.isLoggedIn}
|
||||
isInsightSupported={isInsightSupported}
|
||||
/>
|
||||
);
|
||||
}
|
||||
+8
-1
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import type { ProjectMeta } from "../../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { ErrorPage, LoadingPage } from "../../_components/page-skeletons";
|
||||
import { RedirectToContractOverview } from "../../_components/redirect-contract-overview.client";
|
||||
import { useContractPageMetadata } from "../../_hooks/useContractPageMetadata";
|
||||
@@ -9,6 +10,7 @@ import { ContractEnglishAuctionsPage } from "./ContractEnglishAuctionsPage";
|
||||
export function ContractEnglishAuctionsPageClient(props: {
|
||||
contract: ThirdwebContract;
|
||||
isLoggedIn: boolean;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
}) {
|
||||
const metadataQuery = useContractPageMetadata(props.contract);
|
||||
|
||||
@@ -21,7 +23,12 @@ export function ContractEnglishAuctionsPageClient(props: {
|
||||
}
|
||||
|
||||
if (!metadataQuery.data.isEnglishAuctionSupported) {
|
||||
return <RedirectToContractOverview contract={props.contract} />;
|
||||
return (
|
||||
<RedirectToContractOverview
|
||||
contract={props.contract}
|
||||
projectMeta={props.projectMeta}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
+9
-37
@@ -1,46 +1,18 @@
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { getRawAccount } from "../../../../../../account/settings/getAccount";
|
||||
import { getContractPageParamsInfo } from "../../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../../_utils/getContractPageMetadata";
|
||||
import { ContractEnglishAuctionsPage } from "./ContractEnglishAuctionsPage";
|
||||
import { ContractEnglishAuctionsPageClient } from "./ContractEnglishAuctionsPage.client";
|
||||
import type { PublicContractPageParams } from "../../types";
|
||||
import { SharedEnglishAuctionsPage } from "./shared-english-auctions-page";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: Promise<{
|
||||
contractAddress: string;
|
||||
chain_id: string;
|
||||
}>;
|
||||
params: Promise<PublicContractPageParams>;
|
||||
}) {
|
||||
const params = await props.params;
|
||||
const info = await getContractPageParamsInfo(params);
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const twAccount = await getRawAccount();
|
||||
|
||||
if (info.isLocalhostChain) {
|
||||
return (
|
||||
<ContractEnglishAuctionsPageClient
|
||||
contract={info.clientContract}
|
||||
isLoggedIn={!!twAccount}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const { isEnglishAuctionSupported, isInsightSupported } =
|
||||
await getContractPageMetadata(info.serverContract);
|
||||
|
||||
if (!isEnglishAuctionSupported) {
|
||||
redirect(`/${params.chain_id}/${params.contractAddress}`);
|
||||
}
|
||||
const [params, account] = await Promise.all([props.params, getRawAccount()]);
|
||||
|
||||
return (
|
||||
<ContractEnglishAuctionsPage
|
||||
contract={info.clientContract}
|
||||
isLoggedIn={!!twAccount}
|
||||
isInsightSupported={isInsightSupported}
|
||||
<SharedEnglishAuctionsPage
|
||||
contractAddress={params.contractAddress}
|
||||
chainIdOrSlug={params.chain_id}
|
||||
projectMeta={undefined}
|
||||
isLoggedIn={!!account}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import type { ProjectMeta } from "../../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { redirectToContractLandingPage } from "../../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/utils";
|
||||
import { getContractPageParamsInfo } from "../../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../../_utils/getContractPageMetadata";
|
||||
import { ContractEnglishAuctionsPage } from "./ContractEnglishAuctionsPage";
|
||||
import { ContractEnglishAuctionsPageClient } from "./ContractEnglishAuctionsPage.client";
|
||||
|
||||
export async function SharedEnglishAuctionsPage(props: {
|
||||
contractAddress: string;
|
||||
chainIdOrSlug: string;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
isLoggedIn: boolean;
|
||||
}) {
|
||||
const info = await getContractPageParamsInfo({
|
||||
contractAddress: props.contractAddress,
|
||||
chainIdOrSlug: props.chainIdOrSlug,
|
||||
teamId: props.projectMeta?.teamId,
|
||||
});
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
if (info.isLocalhostChain) {
|
||||
return (
|
||||
<ContractEnglishAuctionsPageClient
|
||||
contract={info.clientContract}
|
||||
isLoggedIn={props.isLoggedIn}
|
||||
projectMeta={props.projectMeta}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const metadata = await getContractPageMetadata(info.serverContract);
|
||||
|
||||
if (!metadata.isEnglishAuctionSupported) {
|
||||
redirectToContractLandingPage({
|
||||
contractAddress: props.contractAddress,
|
||||
chainIdOrSlug: props.chainIdOrSlug,
|
||||
projectMeta: props.projectMeta,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<ContractEnglishAuctionsPage
|
||||
contract={info.clientContract}
|
||||
isLoggedIn={props.isLoggedIn}
|
||||
isInsightSupported={metadata.isInsightSupported}
|
||||
/>
|
||||
);
|
||||
}
|
||||
+10
-1
@@ -3,6 +3,8 @@ import { TrackedLinkTW } from "@/components/ui/tracked-link";
|
||||
import { useMemo } from "react";
|
||||
import { type NFT, ZERO_ADDRESS } from "thirdweb";
|
||||
import { NFTMediaWithEmptyState } from "tw-components/nft-media";
|
||||
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { buildContractPagePath } from "../_utils/contract-page-path";
|
||||
|
||||
type NFTWithContract = NFT & { contractAddress: string; chainId: number };
|
||||
|
||||
@@ -29,6 +31,7 @@ interface NFTCardsProps {
|
||||
trackingCategory: string;
|
||||
isPending: boolean;
|
||||
allNfts?: boolean;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
}
|
||||
|
||||
export const NFTCards: React.FC<NFTCardsProps> = ({
|
||||
@@ -36,6 +39,7 @@ export const NFTCards: React.FC<NFTCardsProps> = ({
|
||||
trackingCategory,
|
||||
isPending,
|
||||
allNfts,
|
||||
projectMeta,
|
||||
}) => {
|
||||
const dummyData = useMemo(() => {
|
||||
return Array.from({
|
||||
@@ -87,7 +91,12 @@ export const NFTCards: React.FC<NFTCardsProps> = ({
|
||||
<TrackedLinkTW
|
||||
category={trackingCategory}
|
||||
label="view_nft"
|
||||
href={`/${token.chainId}/${token.contractAddress}/nfts/${tokenId}`}
|
||||
href={buildContractPagePath({
|
||||
projectMeta,
|
||||
chainIdOrSlug: token.chainId.toString(),
|
||||
contractAddress: token.contractAddress,
|
||||
subpath: `/nfts/${tokenId}`,
|
||||
})}
|
||||
className="before:absolute before:inset-0"
|
||||
>
|
||||
{v.name}
|
||||
|
||||
+11
-2
@@ -3,10 +3,13 @@
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
import { useEffect, useRef } from "react";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { buildContractPagePath } from "../_utils/contract-page-path";
|
||||
import { LoadingPage } from "./page-skeletons";
|
||||
|
||||
export function RedirectToContractOverview(props: {
|
||||
contract: ThirdwebContract;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
}) {
|
||||
const router = useDashboardRouter();
|
||||
const redirected = useRef(false);
|
||||
@@ -17,8 +20,14 @@ export function RedirectToContractOverview(props: {
|
||||
return;
|
||||
}
|
||||
redirected.current = true;
|
||||
router.replace(`/${props.contract.chain.id}/${props.contract.address}`);
|
||||
}, [router, props.contract]);
|
||||
const landingPage = buildContractPagePath({
|
||||
projectMeta: props.projectMeta,
|
||||
chainIdOrSlug: props.contract.chain.id.toString(),
|
||||
contractAddress: props.contract.address,
|
||||
});
|
||||
|
||||
router.replace(landingPage);
|
||||
}, [router, props]);
|
||||
|
||||
return <LoadingPage />;
|
||||
}
|
||||
|
||||
+4
-2
@@ -2,8 +2,9 @@
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import type { MinimalTeamsAndProjects } from "components/contract-components/contract-deploy-form/add-to-project-card";
|
||||
import type { ThirdwebClient, ThirdwebContract } from "thirdweb";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import type { ChainMetadata } from "thirdweb/chains";
|
||||
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { ErrorPage, LoadingPage } from "../_components/page-skeletons";
|
||||
import { useContractPageMetadata } from "../_hooks/useContractPageMetadata";
|
||||
import { getContractPageSidebarLinks } from "../_utils/getContractPageSidebarLinks";
|
||||
@@ -15,7 +16,7 @@ export function ContractPageLayoutClient(props: {
|
||||
contract: ThirdwebContract;
|
||||
children: React.ReactNode;
|
||||
teamsAndProjects: MinimalTeamsAndProjects | undefined;
|
||||
client: ThirdwebClient;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
}) {
|
||||
const metadataQuery = useContractPageMetadata(props.contract);
|
||||
const headerMetadataQuery = useQuery({
|
||||
@@ -39,6 +40,7 @@ export function ContractPageLayoutClient(props: {
|
||||
chainSlug: props.chainMetadata.slug,
|
||||
contractAddress: props.contract.address,
|
||||
metadata: metadataQuery.data,
|
||||
projectMeta: props.projectMeta,
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
+6
-4
@@ -4,8 +4,9 @@ import { SidebarLayout } from "@/components/blocks/SidebarLayout";
|
||||
import type { DashboardContractMetadata } from "@3rdweb-sdk/react/hooks/useDashboardContractMetadata";
|
||||
import type { MinimalTeamsAndProjects } from "components/contract-components/contract-deploy-form/add-to-project-card";
|
||||
import { DeprecatedAlert } from "components/shared/DeprecatedAlert";
|
||||
import type { ThirdwebClient, ThirdwebContract } from "thirdweb";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import type { ChainMetadata } from "thirdweb/chains";
|
||||
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { ContractMetadata } from "./contract-metadata";
|
||||
import { PrimaryDashboardButton } from "./primary-dashboard-button";
|
||||
|
||||
@@ -15,8 +16,8 @@ export function ContractPageLayout(props: {
|
||||
children: React.ReactNode;
|
||||
sidebarLinks: SidebarLink[];
|
||||
dashboardContractMetadata: DashboardContractMetadata | undefined;
|
||||
client: ThirdwebClient;
|
||||
teamsAndProjects: MinimalTeamsAndProjects | undefined;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
externalLinks:
|
||||
| {
|
||||
name: string;
|
||||
@@ -31,7 +32,7 @@ export function ContractPageLayout(props: {
|
||||
dashboardContractMetadata,
|
||||
externalLinks,
|
||||
teamsAndProjects,
|
||||
client,
|
||||
projectMeta,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
@@ -46,6 +47,7 @@ export function ContractPageLayout(props: {
|
||||
externalLinks={externalLinks}
|
||||
/>
|
||||
<PrimaryDashboardButton
|
||||
projectMeta={projectMeta}
|
||||
contractAddress={contract.address}
|
||||
chain={contract.chain}
|
||||
contractInfo={{
|
||||
@@ -54,7 +56,7 @@ export function ContractPageLayout(props: {
|
||||
contractAddress: contract.address,
|
||||
}}
|
||||
teamsAndProjects={teamsAndProjects}
|
||||
client={client}
|
||||
client={contract.client}
|
||||
/>
|
||||
</div>
|
||||
<DeprecatedAlert chain={chainMetadata} />
|
||||
|
||||
+17
-3
@@ -26,6 +26,8 @@ import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import type { Chain, ThirdwebClient } from "thirdweb";
|
||||
import { useAddContractToProject } from "../../../../../team/[team_slug]/[project_slug]/(sidebar)/hooks/project-contracts";
|
||||
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { buildContractPagePath } from "../_utils/contract-page-path";
|
||||
|
||||
const TRACKING_CATEGORY = "add_to_dashboard_upsell";
|
||||
|
||||
@@ -36,6 +38,7 @@ type AddToDashboardCardProps = {
|
||||
hideCodePageLink?: boolean;
|
||||
teamsAndProjects: MinimalTeamsAndProjects | undefined;
|
||||
client: ThirdwebClient;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
};
|
||||
|
||||
export const PrimaryDashboardButton: React.FC<AddToDashboardCardProps> = ({
|
||||
@@ -45,9 +48,17 @@ export const PrimaryDashboardButton: React.FC<AddToDashboardCardProps> = ({
|
||||
hideCodePageLink,
|
||||
teamsAndProjects,
|
||||
client,
|
||||
projectMeta,
|
||||
}) => {
|
||||
const pathname = usePathname();
|
||||
|
||||
const codePagePath = buildContractPagePath({
|
||||
projectMeta,
|
||||
chainIdOrSlug: contractInfo.chainSlug,
|
||||
contractAddress: contractAddress,
|
||||
subpath: "/code",
|
||||
});
|
||||
|
||||
// if user is not logged in
|
||||
if (!teamsAndProjects) {
|
||||
if (hideCodePageLink) {
|
||||
@@ -57,9 +68,7 @@ export const PrimaryDashboardButton: React.FC<AddToDashboardCardProps> = ({
|
||||
if (!pathname?.endsWith("/code")) {
|
||||
return (
|
||||
<Button variant="outline" asChild className="gap-2">
|
||||
<Link
|
||||
href={`/${contractInfo.chainSlug}/${contractInfo.contractAddress}/code`}
|
||||
>
|
||||
<Link href={codePagePath}>
|
||||
<CodeIcon className="size-4" />
|
||||
Code Snippets
|
||||
</Link>
|
||||
@@ -70,6 +79,11 @@ export const PrimaryDashboardButton: React.FC<AddToDashboardCardProps> = ({
|
||||
return null;
|
||||
}
|
||||
|
||||
// if user is on a project page
|
||||
if (projectMeta) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<AddToProjectButton
|
||||
contractAddress={contractAddress}
|
||||
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
|
||||
export function buildContractPagePath(params: {
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
chainIdOrSlug: string;
|
||||
contractAddress: string;
|
||||
subpath?: `/${string}`;
|
||||
}) {
|
||||
const { projectMeta, chainIdOrSlug, contractAddress, subpath } = params;
|
||||
if (projectMeta) {
|
||||
return `/team/${projectMeta.teamSlug}/${projectMeta.projectSlug}/contract/${chainIdOrSlug}/${contractAddress}${subpath || ""}`;
|
||||
}
|
||||
|
||||
return `/${chainIdOrSlug}/${contractAddress}${subpath || ""}`;
|
||||
}
|
||||
+6
-3
@@ -7,10 +7,11 @@ import { getUserThirdwebClient } from "../../../../../api/lib/getAuthToken";
|
||||
|
||||
export async function getContractPageParamsInfo(params: {
|
||||
contractAddress: string;
|
||||
chain_id: string;
|
||||
chainIdOrSlug: string;
|
||||
teamId: string | undefined;
|
||||
}) {
|
||||
const contractAddress = getCheckSummedAddress(params.contractAddress);
|
||||
const chainSlugOrId = params.chain_id;
|
||||
const chainSlugOrId = params.chainIdOrSlug;
|
||||
const chainMetadata = await fetchChainWithLocalOverrides(chainSlugOrId);
|
||||
|
||||
if (!chainMetadata) {
|
||||
@@ -31,7 +32,9 @@ export async function getContractPageParamsInfo(params: {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
chain: mapV4ChainToV5Chain(chainMetadata),
|
||||
// if we have the auth token pass it into the client
|
||||
client: await getUserThirdwebClient(),
|
||||
client: await getUserThirdwebClient({
|
||||
teamId: params.teamId,
|
||||
}),
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
+8
-1
@@ -1,4 +1,6 @@
|
||||
import type { SidebarBaseLink, SidebarLink } from "@/components/blocks/Sidebar";
|
||||
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { buildContractPagePath } from "./contract-page-path";
|
||||
import type { ContractPageMetadata } from "./getContractPageMetadata";
|
||||
|
||||
type SidebarBaseLinkWithHide = SidebarBaseLink & {
|
||||
@@ -9,8 +11,13 @@ export function getContractPageSidebarLinks(data: {
|
||||
metadata: ContractPageMetadata;
|
||||
contractAddress: string;
|
||||
chainSlug: string;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
}) {
|
||||
const layoutPrefix = `/${data.chainSlug}/${data.contractAddress}`;
|
||||
const layoutPrefix = buildContractPagePath({
|
||||
projectMeta: data.projectMeta,
|
||||
chainIdOrSlug: data.chainSlug,
|
||||
contractAddress: data.contractAddress,
|
||||
});
|
||||
|
||||
const generalLinks: SidebarBaseLinkWithHide[] = [
|
||||
{
|
||||
|
||||
+8
-1
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { ErrorPage, LoadingPage } from "../_components/page-skeletons";
|
||||
import { RedirectToContractOverview } from "../_components/redirect-contract-overview.client";
|
||||
import { useContractPageMetadata } from "../_hooks/useContractPageMetadata";
|
||||
@@ -8,6 +9,7 @@ import { AccountSigners } from "./components/account-signers";
|
||||
|
||||
export function AccountSignersClient(props: {
|
||||
contract: ThirdwebContract;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
}) {
|
||||
const metadataQuery = useContractPageMetadata(props.contract);
|
||||
|
||||
@@ -20,7 +22,12 @@ export function AccountSignersClient(props: {
|
||||
}
|
||||
|
||||
if (!metadataQuery.data.isAccountPermissionsSupported) {
|
||||
return <RedirectToContractOverview contract={props.contract} />;
|
||||
return (
|
||||
<RedirectToContractOverview
|
||||
contract={props.contract}
|
||||
projectMeta={props.projectMeta}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <AccountSigners contract={props.contract} />;
|
||||
|
||||
+10
-29
@@ -1,34 +1,15 @@
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
|
||||
import { AccountSignersClient } from "./AccountSigners.client";
|
||||
import { AccountSigners } from "./components/account-signers";
|
||||
import type { PublicContractPageParams } from "../types";
|
||||
import { SharedContractAccountPermissionsPage } from "./shared-account-permissions-page";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: Promise<{
|
||||
contractAddress: string;
|
||||
chain_id: string;
|
||||
}>;
|
||||
params: Promise<PublicContractPageParams>;
|
||||
}) {
|
||||
const params = await props.params;
|
||||
const info = await getContractPageParamsInfo(params);
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const { clientContract, serverContract, isLocalhostChain } = info;
|
||||
|
||||
if (isLocalhostChain) {
|
||||
return <AccountSignersClient contract={clientContract} />;
|
||||
}
|
||||
|
||||
const { isAccountPermissionsSupported } =
|
||||
await getContractPageMetadata(serverContract);
|
||||
|
||||
if (!isAccountPermissionsSupported) {
|
||||
redirect(`/${params.chain_id}/${params.contractAddress}`);
|
||||
}
|
||||
|
||||
return <AccountSigners contract={clientContract} />;
|
||||
return (
|
||||
<SharedContractAccountPermissionsPage
|
||||
contractAddress={params.contractAddress}
|
||||
chainIdOrSlug={params.chain_id}
|
||||
projectMeta={undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { redirectToContractLandingPage } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/utils";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
|
||||
import { AccountSignersClient } from "./AccountSigners.client";
|
||||
import { AccountSigners } from "./components/account-signers";
|
||||
|
||||
export async function SharedContractAccountPermissionsPage(props: {
|
||||
contractAddress: string;
|
||||
chainIdOrSlug: string;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
}) {
|
||||
const info = await getContractPageParamsInfo({
|
||||
contractAddress: props.contractAddress,
|
||||
chainIdOrSlug: props.chainIdOrSlug,
|
||||
teamId: props.projectMeta?.teamId,
|
||||
});
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const { clientContract, serverContract, isLocalhostChain } = info;
|
||||
|
||||
if (isLocalhostChain) {
|
||||
return (
|
||||
<AccountSignersClient
|
||||
contract={clientContract}
|
||||
projectMeta={props.projectMeta}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const { isAccountPermissionsSupported } =
|
||||
await getContractPageMetadata(serverContract);
|
||||
|
||||
if (!isAccountPermissionsSupported) {
|
||||
redirectToContractLandingPage({
|
||||
chainIdOrSlug: props.chainIdOrSlug,
|
||||
contractAddress: props.contractAddress,
|
||||
projectMeta: props.projectMeta,
|
||||
});
|
||||
}
|
||||
|
||||
return <AccountSigners contract={clientContract} />;
|
||||
}
|
||||
+9
-1
@@ -2,6 +2,7 @@
|
||||
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import type { ChainMetadata } from "thirdweb/chains";
|
||||
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { ErrorPage, LoadingPage } from "../_components/page-skeletons";
|
||||
import { RedirectToContractOverview } from "../_components/redirect-contract-overview.client";
|
||||
import { useContractPageMetadata } from "../_hooks/useContractPageMetadata";
|
||||
@@ -11,6 +12,7 @@ export function AccountPageClient(props: {
|
||||
contract: ThirdwebContract;
|
||||
chainMetadata: ChainMetadata;
|
||||
isLoggedIn: boolean;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
}) {
|
||||
const metadataQuery = useContractPageMetadata(props.contract);
|
||||
|
||||
@@ -23,7 +25,12 @@ export function AccountPageClient(props: {
|
||||
}
|
||||
|
||||
if (!metadataQuery.data.isAccount) {
|
||||
return <RedirectToContractOverview contract={props.contract} />;
|
||||
return (
|
||||
<RedirectToContractOverview
|
||||
contract={props.contract}
|
||||
projectMeta={props.projectMeta}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -32,6 +39,7 @@ export function AccountPageClient(props: {
|
||||
chainMetadata={props.chainMetadata}
|
||||
isLoggedIn={props.isLoggedIn}
|
||||
isInsightSupported={metadataQuery.data.isInsightSupported}
|
||||
projectMeta={props.projectMeta}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+8
-1
@@ -3,6 +3,7 @@
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import type { ChainMetadata } from "thirdweb/chains";
|
||||
import { Heading } from "tw-components";
|
||||
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { AccountBalance } from "./components/account-balance";
|
||||
import { DepositNative } from "./components/deposit-native";
|
||||
import { NftsOwned } from "./components/nfts-owned";
|
||||
@@ -12,6 +13,7 @@ interface AccountPageProps {
|
||||
chainMetadata: ChainMetadata;
|
||||
isLoggedIn: boolean;
|
||||
isInsightSupported: boolean;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
}
|
||||
|
||||
export const AccountPage: React.FC<AccountPageProps> = ({
|
||||
@@ -19,6 +21,7 @@ export const AccountPage: React.FC<AccountPageProps> = ({
|
||||
chainMetadata,
|
||||
isLoggedIn,
|
||||
isInsightSupported,
|
||||
projectMeta,
|
||||
}) => {
|
||||
const symbol = chainMetadata.nativeCurrency.symbol || "Native Token";
|
||||
|
||||
@@ -44,7 +47,11 @@ export const AccountPage: React.FC<AccountPageProps> = ({
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
<Heading size="title.sm">NFTs owned</Heading>
|
||||
</div>
|
||||
<NftsOwned contract={contract} isInsightSupported={isInsightSupported} />
|
||||
<NftsOwned
|
||||
contract={contract}
|
||||
isInsightSupported={isInsightSupported}
|
||||
projectMeta={projectMeta}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
+4
@@ -2,16 +2,19 @@
|
||||
|
||||
import { useWalletNFTs } from "@3rdweb-sdk/react";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import type { ProjectMeta } from "../../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { NFTCards } from "../../_components/NFTCards";
|
||||
|
||||
interface NftsOwnedProps {
|
||||
contract: ThirdwebContract;
|
||||
isInsightSupported: boolean;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
}
|
||||
|
||||
export const NftsOwned: React.FC<NftsOwnedProps> = ({
|
||||
contract,
|
||||
isInsightSupported,
|
||||
projectMeta,
|
||||
}) => {
|
||||
const { data: walletNFTs, isPending: isWalletNFTsLoading } = useWalletNFTs({
|
||||
chainId: contract.chain.id,
|
||||
@@ -24,6 +27,7 @@ export const NftsOwned: React.FC<NftsOwnedProps> = ({
|
||||
|
||||
return nfts.length !== 0 ? (
|
||||
<NFTCards
|
||||
projectMeta={projectMeta}
|
||||
nfts={nfts.map((nft) => ({
|
||||
id: BigInt(nft.id),
|
||||
supply: BigInt(nft.supply),
|
||||
|
||||
+8
-42
@@ -1,51 +1,17 @@
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { getRawAccount } from "../../../../../account/settings/getAccount";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
|
||||
import { AccountPage } from "./AccountPage";
|
||||
import { AccountPageClient } from "./AccountPage.client";
|
||||
import type { PublicContractPageParams } from "../types";
|
||||
import { SharedContractAccountPage } from "./shared-account-page";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: Promise<{
|
||||
contractAddress: string;
|
||||
chain_id: string;
|
||||
}>;
|
||||
params: Promise<PublicContractPageParams>;
|
||||
}) {
|
||||
const params = await props.params;
|
||||
const info = await getContractPageParamsInfo(params);
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const { clientContract, serverContract, chainMetadata, isLocalhostChain } =
|
||||
info;
|
||||
|
||||
const account = await getRawAccount();
|
||||
|
||||
if (isLocalhostChain) {
|
||||
return (
|
||||
<AccountPageClient
|
||||
contract={clientContract}
|
||||
chainMetadata={chainMetadata}
|
||||
isLoggedIn={!!account}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const { isAccount, isInsightSupported } =
|
||||
await getContractPageMetadata(serverContract);
|
||||
|
||||
if (!isAccount) {
|
||||
redirect(`/${params.chain_id}/${params.contractAddress}`);
|
||||
}
|
||||
|
||||
const [params, account] = await Promise.all([props.params, getRawAccount()]);
|
||||
return (
|
||||
<AccountPage
|
||||
contract={clientContract}
|
||||
chainMetadata={chainMetadata}
|
||||
<SharedContractAccountPage
|
||||
contractAddress={params.contractAddress}
|
||||
chainIdOrSlug={params.chain_id}
|
||||
projectMeta={undefined}
|
||||
isLoggedIn={!!account}
|
||||
isInsightSupported={isInsightSupported}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { redirectToContractLandingPage } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/utils";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
|
||||
import { AccountPage } from "./AccountPage";
|
||||
import { AccountPageClient } from "./AccountPage.client";
|
||||
|
||||
export async function SharedContractAccountPage(props: {
|
||||
contractAddress: string;
|
||||
chainIdOrSlug: string;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
isLoggedIn: boolean;
|
||||
}) {
|
||||
const info = await getContractPageParamsInfo({
|
||||
contractAddress: props.contractAddress,
|
||||
chainIdOrSlug: props.chainIdOrSlug,
|
||||
teamId: props.projectMeta?.teamId,
|
||||
});
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
if (info.isLocalhostChain) {
|
||||
return (
|
||||
<AccountPageClient
|
||||
contract={info.clientContract}
|
||||
chainMetadata={info.chainMetadata}
|
||||
isLoggedIn={props.isLoggedIn}
|
||||
projectMeta={props.projectMeta}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const { isAccount, isInsightSupported } = await getContractPageMetadata(
|
||||
info.serverContract,
|
||||
);
|
||||
|
||||
if (!isAccount) {
|
||||
redirectToContractLandingPage({
|
||||
chainIdOrSlug: props.chainIdOrSlug,
|
||||
contractAddress: props.contractAddress,
|
||||
projectMeta: props.projectMeta,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<AccountPage
|
||||
contract={info.clientContract}
|
||||
chainMetadata={info.chainMetadata}
|
||||
isLoggedIn={props.isLoggedIn}
|
||||
isInsightSupported={isInsightSupported}
|
||||
projectMeta={props.projectMeta}
|
||||
/>
|
||||
);
|
||||
}
|
||||
+13
-2
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { ErrorPage, LoadingPage } from "../_components/page-skeletons";
|
||||
import { RedirectToContractOverview } from "../_components/redirect-contract-overview.client";
|
||||
import { useContractPageMetadata } from "../_hooks/useContractPageMetadata";
|
||||
@@ -9,6 +10,7 @@ import { AccountsPage } from "./AccountsPage";
|
||||
export function AccountsPageClient(props: {
|
||||
contract: ThirdwebContract;
|
||||
isLoggedIn: boolean;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
}) {
|
||||
const metadataQuery = useContractPageMetadata(props.contract);
|
||||
|
||||
@@ -21,10 +23,19 @@ export function AccountsPageClient(props: {
|
||||
}
|
||||
|
||||
if (!metadataQuery.data.isAccountFactory) {
|
||||
return <RedirectToContractOverview contract={props.contract} />;
|
||||
return (
|
||||
<RedirectToContractOverview
|
||||
contract={props.contract}
|
||||
projectMeta={props.projectMeta}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<AccountsPage contract={props.contract} isLoggedIn={props.isLoggedIn} />
|
||||
<AccountsPage
|
||||
contract={props.contract}
|
||||
isLoggedIn={props.isLoggedIn}
|
||||
projectMeta={props.projectMeta}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+4
-1
@@ -3,6 +3,7 @@
|
||||
import { ButtonGroup, Flex } from "@chakra-ui/react";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import { Heading, TrackedLinkButton } from "tw-components";
|
||||
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { AccountsCount } from "./components/accounts-count";
|
||||
import { AccountsTable } from "./components/accounts-table";
|
||||
import { CreateAccountButton } from "./components/create-account-button";
|
||||
@@ -10,11 +11,13 @@ import { CreateAccountButton } from "./components/create-account-button";
|
||||
interface AccountsPageProps {
|
||||
contract: ThirdwebContract;
|
||||
isLoggedIn: boolean;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
}
|
||||
|
||||
export const AccountsPage: React.FC<AccountsPageProps> = ({
|
||||
contract,
|
||||
isLoggedIn,
|
||||
projectMeta,
|
||||
}) => {
|
||||
return (
|
||||
<Flex direction="column" gap={6}>
|
||||
@@ -43,7 +46,7 @@ export const AccountsPage: React.FC<AccountsPageProps> = ({
|
||||
</ButtonGroup>
|
||||
</Flex>
|
||||
<AccountsCount contract={contract} />
|
||||
<AccountsTable contract={contract} />
|
||||
<AccountsTable contract={contract} projectMeta={projectMeta} />
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
+8
-16
@@ -1,16 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
SimpleGrid,
|
||||
Skeleton,
|
||||
Stat,
|
||||
StatLabel,
|
||||
StatNumber,
|
||||
} from "@chakra-ui/react";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import { totalAccounts } from "thirdweb/extensions/erc4337";
|
||||
import { useReadContract } from "thirdweb/react";
|
||||
import { Card } from "tw-components";
|
||||
import { StatCard } from "../../overview/components/stat-card";
|
||||
|
||||
type AccountsCountProps = {
|
||||
contract: ThirdwebContract;
|
||||
@@ -19,13 +12,12 @@ type AccountsCountProps = {
|
||||
export const AccountsCount: React.FC<AccountsCountProps> = ({ contract }) => {
|
||||
const totalAccountsQuery = useReadContract(totalAccounts, { contract });
|
||||
return (
|
||||
<SimpleGrid columns={{ base: 1, md: 3 }} spacing={{ base: 3, md: 6 }}>
|
||||
<Card as={Stat}>
|
||||
<StatLabel mb={{ base: 1, md: 0 }}>Total Accounts</StatLabel>
|
||||
<Skeleton isLoaded={totalAccountsQuery.isSuccess}>
|
||||
<StatNumber>{totalAccountsQuery.data?.toString()}</StatNumber>
|
||||
</Skeleton>
|
||||
</Card>
|
||||
</SimpleGrid>
|
||||
<div className="grid grid-cols-1 gap-3 md:grid-cols-3 md:gap-6">
|
||||
<StatCard
|
||||
label="Total Accounts"
|
||||
value={totalAccountsQuery.data?.toString() || "0"}
|
||||
isPending={totalAccountsQuery.isPending}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
+14
-2
@@ -16,6 +16,8 @@ import type { ThirdwebContract } from "thirdweb";
|
||||
import { getAccounts, totalAccounts } from "thirdweb/extensions/erc4337";
|
||||
import { useReadContract } from "thirdweb/react";
|
||||
import { Text, TrackedCopyButton } from "tw-components";
|
||||
import type { ProjectMeta } from "../../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { buildContractPagePath } from "../../_utils/contract-page-path";
|
||||
|
||||
const columnHelper = createColumnHelper<{ account: string }>();
|
||||
|
||||
@@ -38,9 +40,13 @@ const columns = [
|
||||
|
||||
type AccountsTableProps = {
|
||||
contract: ThirdwebContract;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
};
|
||||
|
||||
export const AccountsTable: React.FC<AccountsTableProps> = ({ contract }) => {
|
||||
export const AccountsTable: React.FC<AccountsTableProps> = ({
|
||||
contract,
|
||||
projectMeta,
|
||||
}) => {
|
||||
const router = useDashboardRouter();
|
||||
const chainSlug = useChainSlug(contract.chain.id);
|
||||
|
||||
@@ -92,7 +98,13 @@ export const AccountsTable: React.FC<AccountsTableProps> = ({ contract }) => {
|
||||
isPending={accountsQuery.isPending}
|
||||
isFetched={accountsQuery.isFetched}
|
||||
onRowClick={(row) => {
|
||||
router.push(`/${chainSlug}/${row.account}`);
|
||||
const accountContractPagePath = buildContractPagePath({
|
||||
projectMeta,
|
||||
chainIdOrSlug: chainSlug.toString(),
|
||||
contractAddress: row.account,
|
||||
});
|
||||
|
||||
router.push(accountContractPagePath);
|
||||
}}
|
||||
/>
|
||||
{/* pagination */}
|
||||
|
||||
+12
-36
@@ -1,41 +1,17 @@
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { getRawAccount } from "../../../../../account/settings/getAccount";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
|
||||
import { AccountsPage } from "./AccountsPage";
|
||||
import { AccountsPageClient } from "./AccountsPage.client";
|
||||
import type { PublicContractPageParams } from "../types";
|
||||
import { SharedAccountsPage } from "./shared-accounts-page";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: Promise<{
|
||||
contractAddress: string;
|
||||
chain_id: string;
|
||||
}>;
|
||||
params: Promise<PublicContractPageParams>;
|
||||
}) {
|
||||
const params = await props.params;
|
||||
const info = await getContractPageParamsInfo(params);
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const { clientContract, serverContract, isLocalhostChain } = info;
|
||||
|
||||
const [account, { isAccountFactory }] = await Promise.all([
|
||||
getRawAccount(),
|
||||
isLocalhostChain
|
||||
? { isAccountFactory: undefined }
|
||||
: getContractPageMetadata(serverContract),
|
||||
]);
|
||||
|
||||
if (isLocalhostChain) {
|
||||
return (
|
||||
<AccountsPageClient contract={clientContract} isLoggedIn={!!account} />
|
||||
);
|
||||
}
|
||||
|
||||
if (!isAccountFactory) {
|
||||
redirect(`/${params.chain_id}/${params.contractAddress}`);
|
||||
}
|
||||
|
||||
return <AccountsPage contract={clientContract} isLoggedIn={!!account} />;
|
||||
const [params, account] = await Promise.all([props.params, getRawAccount()]);
|
||||
return (
|
||||
<SharedAccountsPage
|
||||
contractAddress={params.contractAddress}
|
||||
chainIdOrSlug={params.chain_id}
|
||||
projectMeta={undefined}
|
||||
isLoggedIn={!!account}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { redirectToContractLandingPage } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/utils";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
|
||||
import { AccountsPage } from "./AccountsPage";
|
||||
import { AccountsPageClient } from "./AccountsPage.client";
|
||||
|
||||
export async function SharedAccountsPage(props: {
|
||||
contractAddress: string;
|
||||
chainIdOrSlug: string;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
isLoggedIn: boolean;
|
||||
}) {
|
||||
const info = await getContractPageParamsInfo({
|
||||
contractAddress: props.contractAddress,
|
||||
chainIdOrSlug: props.chainIdOrSlug,
|
||||
teamId: props.projectMeta?.teamId,
|
||||
});
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const { clientContract, serverContract, isLocalhostChain } = info;
|
||||
|
||||
if (isLocalhostChain) {
|
||||
return (
|
||||
<AccountsPageClient
|
||||
contract={clientContract}
|
||||
isLoggedIn={props.isLoggedIn}
|
||||
projectMeta={props.projectMeta}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const { isAccountFactory } = await getContractPageMetadata(serverContract);
|
||||
|
||||
if (!isAccountFactory) {
|
||||
redirectToContractLandingPage({
|
||||
chainIdOrSlug: props.chainIdOrSlug,
|
||||
contractAddress: props.contractAddress,
|
||||
projectMeta: props.projectMeta,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<AccountsPage
|
||||
contract={clientContract}
|
||||
isLoggedIn={props.isLoggedIn}
|
||||
projectMeta={props.projectMeta}
|
||||
/>
|
||||
);
|
||||
}
|
||||
+7
-63
@@ -1,71 +1,15 @@
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { type ThirdwebContract, resolveContractAbi } from "thirdweb/contract";
|
||||
import { type Abi, toEventSelector, toFunctionSelector } from "thirdweb/utils";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
|
||||
import { ContractAnalyticsPage } from "./ContractAnalyticsPage";
|
||||
import type { PublicContractPageParams } from "../types";
|
||||
import { SharedAnalyticsPage } from "./shared-analytics-page";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: Promise<{
|
||||
contractAddress: string;
|
||||
chain_id: string;
|
||||
}>;
|
||||
params: Promise<PublicContractPageParams>;
|
||||
}) {
|
||||
const params = await props.params;
|
||||
const info = await getContractPageParamsInfo(params);
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const [
|
||||
{ eventSelectorToName, writeFnSelectorToName },
|
||||
{ isInsightSupported },
|
||||
] = await Promise.all([
|
||||
getSelectors(info.serverContract),
|
||||
getContractPageMetadata(info.serverContract),
|
||||
]);
|
||||
|
||||
if (!isInsightSupported) {
|
||||
redirect(`/${params.chain_id}/${params.contractAddress}`);
|
||||
}
|
||||
|
||||
return (
|
||||
<ContractAnalyticsPage
|
||||
contract={info.clientContract}
|
||||
writeFnSelectorToNameRecord={writeFnSelectorToName}
|
||||
eventSelectorToNameRecord={eventSelectorToName}
|
||||
<SharedAnalyticsPage
|
||||
contractAddress={params.contractAddress}
|
||||
chainIdOrSlug={params.chain_id}
|
||||
projectMeta={undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
async function getSelectors(contract: ThirdwebContract) {
|
||||
try {
|
||||
const abi = await resolveContractAbi<Abi>(contract);
|
||||
const writeFnSelectorToName: Record<string, string> = {};
|
||||
const eventSelectorToName: Record<string, string> = {};
|
||||
|
||||
for (const item of abi) {
|
||||
if (item.type === "event") {
|
||||
eventSelectorToName[toEventSelector(item)] = item.name;
|
||||
} else if (
|
||||
// if write function
|
||||
item.type === "function" &&
|
||||
item.stateMutability !== "view" &&
|
||||
item.stateMutability !== "pure"
|
||||
) {
|
||||
writeFnSelectorToName[toFunctionSelector(item)] = item.name;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
writeFnSelectorToName,
|
||||
eventSelectorToName,
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
writeFnSelectorToName: {},
|
||||
eventSelectorToName: {},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { type ThirdwebContract, resolveContractAbi } from "thirdweb/contract";
|
||||
import { type Abi, toEventSelector, toFunctionSelector } from "thirdweb/utils";
|
||||
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { redirectToContractLandingPage } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/utils";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
|
||||
import { ContractAnalyticsPage } from "./ContractAnalyticsPage";
|
||||
|
||||
export async function SharedAnalyticsPage(props: {
|
||||
contractAddress: string;
|
||||
chainIdOrSlug: string;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
}) {
|
||||
const info = await getContractPageParamsInfo({
|
||||
contractAddress: props.contractAddress,
|
||||
chainIdOrSlug: props.chainIdOrSlug,
|
||||
teamId: props.projectMeta?.teamId,
|
||||
});
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const [
|
||||
{ eventSelectorToName, writeFnSelectorToName },
|
||||
{ isInsightSupported },
|
||||
] = await Promise.all([
|
||||
getSelectors(info.serverContract),
|
||||
getContractPageMetadata(info.serverContract),
|
||||
]);
|
||||
|
||||
if (!isInsightSupported) {
|
||||
redirectToContractLandingPage({
|
||||
chainIdOrSlug: props.chainIdOrSlug,
|
||||
contractAddress: props.contractAddress,
|
||||
projectMeta: props.projectMeta,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<ContractAnalyticsPage
|
||||
contract={info.clientContract}
|
||||
writeFnSelectorToNameRecord={writeFnSelectorToName}
|
||||
eventSelectorToNameRecord={eventSelectorToName}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
async function getSelectors(contract: ThirdwebContract) {
|
||||
try {
|
||||
const abi = await resolveContractAbi<Abi>(contract);
|
||||
const writeFnSelectorToName: Record<string, string> = {};
|
||||
const eventSelectorToName: Record<string, string> = {};
|
||||
|
||||
for (const item of abi) {
|
||||
if (item.type === "event") {
|
||||
eventSelectorToName[toEventSelector(item)] = item.name;
|
||||
} else if (
|
||||
// if write function
|
||||
item.type === "function" &&
|
||||
item.stateMutability !== "view" &&
|
||||
item.stateMutability !== "pure"
|
||||
) {
|
||||
writeFnSelectorToName[toFunctionSelector(item)] = item.name;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
writeFnSelectorToName,
|
||||
eventSelectorToName,
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
writeFnSelectorToName: {},
|
||||
eventSelectorToName: {},
|
||||
};
|
||||
}
|
||||
}
|
||||
+8
-1
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { ClaimConditions } from "../_components/claim-conditions/claim-conditions";
|
||||
import { ErrorPage, LoadingPage } from "../_components/page-skeletons";
|
||||
import { RedirectToContractOverview } from "../_components/redirect-contract-overview.client";
|
||||
@@ -9,6 +10,7 @@ import { useContractPageMetadata } from "../_hooks/useContractPageMetadata";
|
||||
export function ClaimConditionsClient(props: {
|
||||
contract: ThirdwebContract;
|
||||
isLoggedIn: boolean;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
}) {
|
||||
const metadataQuery = useContractPageMetadata(props.contract);
|
||||
|
||||
@@ -27,7 +29,12 @@ export function ClaimConditionsClient(props: {
|
||||
} = metadataQuery.data;
|
||||
|
||||
if (!isERC20ClaimConditionsSupported && !isERC721ClaimConditionsSupported) {
|
||||
return <RedirectToContractOverview contract={props.contract} />;
|
||||
return (
|
||||
<RedirectToContractOverview
|
||||
contract={props.contract}
|
||||
projectMeta={props.projectMeta}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
+8
-49
@@ -1,58 +1,17 @@
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { getRawAccount } from "../../../../../account/settings/getAccount";
|
||||
import { ClaimConditions } from "../_components/claim-conditions/claim-conditions";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
|
||||
import { ClaimConditionsClient } from "./ClaimConditions.client";
|
||||
import type { PublicContractPageParams } from "../types";
|
||||
import { SharedClaimConditionsPage } from "./shared-claim-conditions-page";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: Promise<{
|
||||
contractAddress: string;
|
||||
chain_id: string;
|
||||
}>;
|
||||
params: Promise<PublicContractPageParams>;
|
||||
}) {
|
||||
const params = await props.params;
|
||||
const info = await getContractPageParamsInfo(params);
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
const { clientContract, serverContract, isLocalhostChain } = info;
|
||||
|
||||
const [
|
||||
account,
|
||||
{
|
||||
isERC20ClaimConditionsSupported,
|
||||
isERC721ClaimConditionsSupported,
|
||||
supportedERCs,
|
||||
},
|
||||
] = await Promise.all([
|
||||
getRawAccount(),
|
||||
isLocalhostChain
|
||||
? {
|
||||
isERC20ClaimConditionsSupported: undefined,
|
||||
isERC721ClaimConditionsSupported: undefined,
|
||||
supportedERCs: undefined,
|
||||
}
|
||||
: getContractPageMetadata(serverContract),
|
||||
]);
|
||||
|
||||
if (isLocalhostChain) {
|
||||
return (
|
||||
<ClaimConditionsClient contract={clientContract} isLoggedIn={!!account} />
|
||||
);
|
||||
}
|
||||
|
||||
if (!isERC20ClaimConditionsSupported && !isERC721ClaimConditionsSupported) {
|
||||
redirect(`/${params.chain_id}/${params.contractAddress}`);
|
||||
}
|
||||
|
||||
const [params, account] = await Promise.all([props.params, getRawAccount()]);
|
||||
return (
|
||||
<ClaimConditions
|
||||
contract={clientContract}
|
||||
isERC20={supportedERCs.isERC20}
|
||||
<SharedClaimConditionsPage
|
||||
contractAddress={params.contractAddress}
|
||||
chainIdOrSlug={params.chain_id}
|
||||
projectMeta={undefined}
|
||||
isLoggedIn={!!account}
|
||||
isMultiphase={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { redirectToContractLandingPage } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/utils";
|
||||
import { ClaimConditions } from "../_components/claim-conditions/claim-conditions";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
|
||||
import { ClaimConditionsClient } from "./ClaimConditions.client";
|
||||
|
||||
export async function SharedClaimConditionsPage(props: {
|
||||
contractAddress: string;
|
||||
chainIdOrSlug: string;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
isLoggedIn: boolean;
|
||||
}) {
|
||||
const info = await getContractPageParamsInfo({
|
||||
contractAddress: props.contractAddress,
|
||||
chainIdOrSlug: props.chainIdOrSlug,
|
||||
teamId: props.projectMeta?.teamId,
|
||||
});
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const { clientContract, serverContract, isLocalhostChain } = info;
|
||||
|
||||
if (isLocalhostChain) {
|
||||
return (
|
||||
<ClaimConditionsClient
|
||||
contract={clientContract}
|
||||
isLoggedIn={props.isLoggedIn}
|
||||
projectMeta={props.projectMeta}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const {
|
||||
isERC20ClaimConditionsSupported,
|
||||
isERC721ClaimConditionsSupported,
|
||||
supportedERCs,
|
||||
} = await getContractPageMetadata(serverContract);
|
||||
|
||||
if (!isERC20ClaimConditionsSupported && !isERC721ClaimConditionsSupported) {
|
||||
redirectToContractLandingPage({
|
||||
chainIdOrSlug: props.chainIdOrSlug,
|
||||
contractAddress: props.contractAddress,
|
||||
projectMeta: props.projectMeta,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<ClaimConditions
|
||||
contract={clientContract}
|
||||
isERC20={supportedERCs.isERC20}
|
||||
isLoggedIn={props.isLoggedIn}
|
||||
isMultiphase={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
+7
-33
@@ -1,41 +1,15 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { resolveContractAbi } from "thirdweb/contract";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { ContractCodePage } from "./contract-code-page";
|
||||
import { ContractCodePageClient } from "./contract-code-page.client";
|
||||
import type { PublicContractPageParams } from "../types";
|
||||
import { SharedCodePage } from "./shared-code-page";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: Promise<{
|
||||
contractAddress: string;
|
||||
chain_id: string;
|
||||
}>;
|
||||
params: Promise<PublicContractPageParams>;
|
||||
}) {
|
||||
const params = await props.params;
|
||||
const info = await getContractPageParamsInfo(params);
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const { clientContract, serverContract, chainMetadata, isLocalhostChain } =
|
||||
info;
|
||||
|
||||
if (isLocalhostChain) {
|
||||
return (
|
||||
<ContractCodePageClient
|
||||
contract={clientContract}
|
||||
chainMetadata={chainMetadata}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const abi = await resolveContractAbi(serverContract).catch(() => undefined);
|
||||
|
||||
return (
|
||||
<ContractCodePage
|
||||
abi={abi}
|
||||
contract={clientContract}
|
||||
chainMetadata={chainMetadata}
|
||||
<SharedCodePage
|
||||
contractAddress={params.contractAddress}
|
||||
chainIdOrSlug={params.chain_id}
|
||||
projectMeta={undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { resolveContractAbi } from "thirdweb/contract";
|
||||
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { ContractCodePage } from "./contract-code-page";
|
||||
import { ContractCodePageClient } from "./contract-code-page.client";
|
||||
|
||||
export async function SharedCodePage(props: {
|
||||
contractAddress: string;
|
||||
chainIdOrSlug: string;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
}) {
|
||||
const info = await getContractPageParamsInfo({
|
||||
contractAddress: props.contractAddress,
|
||||
chainIdOrSlug: props.chainIdOrSlug,
|
||||
teamId: props.projectMeta?.teamId,
|
||||
});
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const { clientContract, serverContract, chainMetadata, isLocalhostChain } =
|
||||
info;
|
||||
|
||||
if (isLocalhostChain) {
|
||||
return (
|
||||
<ContractCodePageClient
|
||||
contract={clientContract}
|
||||
chainMetadata={chainMetadata}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const abi = await resolveContractAbi(serverContract).catch(() => undefined);
|
||||
|
||||
return (
|
||||
<ContractCodePage
|
||||
abi={abi}
|
||||
contract={clientContract}
|
||||
chainMetadata={chainMetadata}
|
||||
/>
|
||||
);
|
||||
}
|
||||
+22
-4
@@ -58,6 +58,8 @@ import {
|
||||
import { useActiveAccount, useSwitchActiveWalletChain } from "thirdweb/react";
|
||||
import { concatHex, padHex } from "thirdweb/utils";
|
||||
import { z } from "zod";
|
||||
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { buildContractPagePath } from "../_utils/contract-page-path";
|
||||
import { SingleNetworkSelector } from "./single-network-selector";
|
||||
|
||||
type CrossChain = {
|
||||
@@ -91,6 +93,7 @@ export function DataTable({
|
||||
inputSalt,
|
||||
initCode,
|
||||
isDirectDeploy,
|
||||
projectMeta,
|
||||
}: {
|
||||
data: CrossChain[];
|
||||
coreMetadata: FetchDeployMetadataResult;
|
||||
@@ -100,6 +103,7 @@ export function DataTable({
|
||||
inputSalt?: `0x${string}`;
|
||||
initCode?: `0x${string}`;
|
||||
isDirectDeploy: boolean;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
}) {
|
||||
const activeAccount = useActiveAccount();
|
||||
const switchChain = useSwitchActiveWalletChain();
|
||||
@@ -235,11 +239,17 @@ export function DataTable({
|
||||
header: "Network",
|
||||
cell: ({ row }) => {
|
||||
if (row.getValue("status") === "DEPLOYED") {
|
||||
const href = buildContractPagePath({
|
||||
projectMeta,
|
||||
chainIdOrSlug: `${row.getValue("chainId")}`,
|
||||
contractAddress: coreContract.address,
|
||||
});
|
||||
|
||||
return (
|
||||
<Link
|
||||
target="_blank"
|
||||
className="text-blue-500 underline"
|
||||
href={`/${row.getValue("chainId")}/${coreContract.address}`}
|
||||
href={href}
|
||||
>
|
||||
{row.getValue("network")}
|
||||
</Link>
|
||||
@@ -405,9 +415,17 @@ export function DataTable({
|
||||
});
|
||||
}
|
||||
deployStatusModal.nextStep();
|
||||
deployStatusModal.setViewContractLink(
|
||||
`/${chain.id}/${crosschainContractAddress}`,
|
||||
);
|
||||
|
||||
if (crosschainContractAddress) {
|
||||
const contractLink = buildContractPagePath({
|
||||
projectMeta,
|
||||
chainIdOrSlug: `${chain.id}`,
|
||||
contractAddress: crosschainContractAddress,
|
||||
});
|
||||
|
||||
deployStatusModal.setViewContractLink(contractLink);
|
||||
}
|
||||
|
||||
deployStatusModal.close();
|
||||
|
||||
setCustomChainData((prevData) =>
|
||||
|
||||
+8
-280
@@ -1,287 +1,15 @@
|
||||
import { fetchPublishedContractsFromDeploy } from "components/contract-components/fetchPublishedContractsFromDeploy";
|
||||
import { notFound } from "next/navigation";
|
||||
import {
|
||||
type ContractOptions,
|
||||
eth_getTransactionByHash,
|
||||
eth_getTransactionReceipt,
|
||||
getContractEvents,
|
||||
parseEventLogs,
|
||||
prepareEvent,
|
||||
} from "thirdweb";
|
||||
import { defineChain, getChainMetadata } from "thirdweb/chains";
|
||||
import {
|
||||
type FetchDeployMetadataResult,
|
||||
getContract,
|
||||
getDeployedCloneFactoryContract,
|
||||
resolveContractAbi,
|
||||
} from "thirdweb/contract";
|
||||
import { moduleInstalledEvent } from "thirdweb/modules";
|
||||
import { eth_getCode, getRpcClient } from "thirdweb/rpc";
|
||||
import type { TransactionReceipt } from "thirdweb/transaction";
|
||||
import { type AbiFunction, decodeFunctionData } from "thirdweb/utils";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
|
||||
import { DataTable } from "./data-table";
|
||||
import { NoCrossChainPrompt } from "./no-crosschain-prompt";
|
||||
|
||||
export function getModuleInstallParams(mod: FetchDeployMetadataResult) {
|
||||
return (
|
||||
mod.abi
|
||||
.filter((a) => a.type === "function")
|
||||
.find((f) => f.name === "encodeBytesOnInstall")?.inputs || []
|
||||
);
|
||||
}
|
||||
import type { PublicContractPageParams } from "../types";
|
||||
import { SharedCrossChainPage } from "./shared-cross-chain-page";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: Promise<{
|
||||
contractAddress: string;
|
||||
chain_id: string;
|
||||
}>;
|
||||
params: Promise<PublicContractPageParams>;
|
||||
}) {
|
||||
const params = await props.params;
|
||||
const info = await getContractPageParamsInfo(params);
|
||||
|
||||
const ProxyDeployedEvent = prepareEvent({
|
||||
signature:
|
||||
"event ProxyDeployedV2(address indexed implementation, address indexed proxy, address indexed deployer, bytes32 inputSalt, bytes data, bytes extraData)",
|
||||
filters: {
|
||||
proxy: params.contractAddress.toLowerCase(),
|
||||
},
|
||||
});
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const { clientContract, serverContract } = info;
|
||||
|
||||
const isModularCore = (await getContractPageMetadata(serverContract))
|
||||
.isModularCore;
|
||||
|
||||
let twCloneFactoryContract: Readonly<
|
||||
ContractOptions<[], `0x${string}`>
|
||||
> | null = null;
|
||||
try {
|
||||
twCloneFactoryContract = await getDeployedCloneFactoryContract({
|
||||
chain: serverContract.chain,
|
||||
client: serverContract.client,
|
||||
});
|
||||
} catch {}
|
||||
|
||||
const originalCode = await eth_getCode(
|
||||
getRpcClient({
|
||||
client: serverContract.client,
|
||||
chain: serverContract.chain,
|
||||
}),
|
||||
{
|
||||
address: serverContract.address,
|
||||
},
|
||||
);
|
||||
|
||||
let initCode: `0x${string}` = "0x";
|
||||
let creationTxReceipt: TransactionReceipt | undefined;
|
||||
let isDirectDeploy = false;
|
||||
try {
|
||||
const res = await fetch(
|
||||
`https://contract.thirdweb-dev.com/creation/${serverContract.chain.id}/${serverContract.address}`,
|
||||
);
|
||||
const creationData = await res.json();
|
||||
|
||||
if (creationData.status === "1" && creationData.result[0]?.txHash) {
|
||||
const rpcClient = getRpcClient({
|
||||
client: serverContract.client,
|
||||
chain: serverContract.chain,
|
||||
});
|
||||
creationTxReceipt = await eth_getTransactionReceipt(rpcClient, {
|
||||
hash: creationData.result[0]?.txHash,
|
||||
});
|
||||
|
||||
const creationTx = await eth_getTransactionByHash(rpcClient, {
|
||||
hash: creationData.result[0]?.txHash,
|
||||
});
|
||||
|
||||
initCode = creationTx.input;
|
||||
isDirectDeploy =
|
||||
creationTx.to?.toLowerCase() ===
|
||||
"0x4e59b44847b379578588920cA78FbF26c0B4956C".toLowerCase();
|
||||
}
|
||||
} catch (e) {
|
||||
console.debug(e);
|
||||
}
|
||||
|
||||
let initializeData: `0x${string}` | undefined;
|
||||
let inputSalt: `0x${string}` | undefined;
|
||||
let creationBlockNumber: bigint | undefined;
|
||||
|
||||
if (twCloneFactoryContract) {
|
||||
const events = await getContractEvents({
|
||||
contract: twCloneFactoryContract,
|
||||
events: [ProxyDeployedEvent],
|
||||
});
|
||||
const event = events.find(
|
||||
(e) =>
|
||||
e.args.proxy.toLowerCase() === params.contractAddress.toLowerCase(),
|
||||
);
|
||||
|
||||
initializeData = event?.args.data;
|
||||
inputSalt = event?.args.inputSalt;
|
||||
creationBlockNumber = event?.blockNumber;
|
||||
}
|
||||
|
||||
const defaultChains = [
|
||||
1, 137, 8453, 10, 42161, 11155111, 84532, 11155420, 421614,
|
||||
];
|
||||
|
||||
const chainsDeployedOn = (
|
||||
await Promise.all(
|
||||
defaultChains.map(async (c) => {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
const chain = defineChain(c);
|
||||
|
||||
try {
|
||||
const chainMetadata = await getChainMetadata(chain);
|
||||
|
||||
const rpcRequest = getRpcClient({
|
||||
client: serverContract.client,
|
||||
chain,
|
||||
});
|
||||
const code = await eth_getCode(rpcRequest, {
|
||||
address: params.contractAddress,
|
||||
});
|
||||
|
||||
return {
|
||||
id: chain.id,
|
||||
network: chainMetadata.name,
|
||||
chainId: chain.id,
|
||||
status:
|
||||
code === originalCode
|
||||
? ("DEPLOYED" as const)
|
||||
: ("NOT_DEPLOYED" as const),
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
id: chain.id,
|
||||
network: "",
|
||||
chainId: chain.id,
|
||||
status: "NOT_DEPLOYED" as const,
|
||||
};
|
||||
}
|
||||
}),
|
||||
)
|
||||
).filter((c) => c.chainId !== serverContract.chain.id);
|
||||
|
||||
let coreMetadata: FetchDeployMetadataResult | undefined;
|
||||
try {
|
||||
coreMetadata = (
|
||||
await fetchPublishedContractsFromDeploy({
|
||||
contract: serverContract,
|
||||
client: serverContract.client,
|
||||
})
|
||||
).at(-1) as FetchDeployMetadataResult;
|
||||
} catch {}
|
||||
|
||||
if (!coreMetadata) {
|
||||
return NoCrossChainPrompt();
|
||||
}
|
||||
|
||||
let modulesMetadata: FetchDeployMetadataResult[] | undefined;
|
||||
|
||||
if (isModularCore) {
|
||||
let modules: string[] | undefined;
|
||||
const moduleEvent = moduleInstalledEvent();
|
||||
// extract module address in ModuleInstalled events from transaction receipt
|
||||
if (creationTxReceipt) {
|
||||
const decodedEvent = parseEventLogs({
|
||||
events: [moduleEvent],
|
||||
logs: creationTxReceipt.logs,
|
||||
});
|
||||
|
||||
modules = decodedEvent.map((e) => e.args.installedModule);
|
||||
}
|
||||
|
||||
// fetch events from contract
|
||||
if (!modules && creationBlockNumber) {
|
||||
const events = await getContractEvents({
|
||||
contract: serverContract,
|
||||
events: [moduleEvent],
|
||||
});
|
||||
|
||||
const filteredEvents = events.filter(
|
||||
(e) => e.blockNumber === creationBlockNumber,
|
||||
);
|
||||
|
||||
modules = filteredEvents.map((e) => e.args.installedModule);
|
||||
}
|
||||
|
||||
// if receipt not available, try extracting module address from initialize data
|
||||
if (!modules && initializeData) {
|
||||
// biome-ignore lint/suspicious/noExplicitAny: FIXME
|
||||
const decodedData: any = await decodeFunctionData({
|
||||
contract: serverContract,
|
||||
data: initializeData,
|
||||
});
|
||||
|
||||
const abi = await resolveContractAbi(serverContract).catch(
|
||||
() => undefined,
|
||||
);
|
||||
|
||||
if (abi) {
|
||||
const initializeFunction = abi.find(
|
||||
(i: AbiFunction) => i.type === "function" && i.name === "initialize",
|
||||
) as unknown as AbiFunction;
|
||||
|
||||
const moduleIndex = initializeFunction.inputs.findIndex(
|
||||
(i) => i.name === "_modules" || i.name === "modules",
|
||||
);
|
||||
|
||||
modules = moduleIndex ? decodedData[moduleIndex] : undefined;
|
||||
}
|
||||
}
|
||||
|
||||
modulesMetadata = modules
|
||||
? ((await Promise.all(
|
||||
modules.map(async (m) =>
|
||||
(
|
||||
await fetchPublishedContractsFromDeploy({
|
||||
contract: getContract({
|
||||
chain: serverContract.chain,
|
||||
client: serverContract.client,
|
||||
address: m,
|
||||
}),
|
||||
client: serverContract.client,
|
||||
})
|
||||
).at(-1),
|
||||
),
|
||||
)) as FetchDeployMetadataResult[])
|
||||
: undefined;
|
||||
}
|
||||
|
||||
if (!isDirectDeploy && !initializeData) {
|
||||
return NoCrossChainPrompt();
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<h2 className="mb-1 font-bold text-2xl tracking-tight">
|
||||
Cross-chain contracts
|
||||
</h2>
|
||||
<p className="text-muted-foreground">
|
||||
Deterministically deploy and interact with your contracts on multiple
|
||||
networks.
|
||||
</p>
|
||||
</div>
|
||||
<div className="h-10" />
|
||||
<DataTable
|
||||
coreMetadata={coreMetadata}
|
||||
modulesMetadata={modulesMetadata}
|
||||
initializeData={initializeData}
|
||||
inputSalt={inputSalt}
|
||||
data={chainsDeployedOn}
|
||||
coreContract={clientContract}
|
||||
initCode={initCode}
|
||||
isDirectDeploy={isDirectDeploy}
|
||||
/>
|
||||
</>
|
||||
<SharedCrossChainPage
|
||||
contractAddress={params.contractAddress}
|
||||
chainIdOrSlug={params.chain_id}
|
||||
projectMeta={undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+282
@@ -0,0 +1,282 @@
|
||||
import { fetchPublishedContractsFromDeploy } from "components/contract-components/fetchPublishedContractsFromDeploy";
|
||||
import { notFound } from "next/navigation";
|
||||
import {
|
||||
type ContractOptions,
|
||||
eth_getTransactionByHash,
|
||||
eth_getTransactionReceipt,
|
||||
getContractEvents,
|
||||
parseEventLogs,
|
||||
prepareEvent,
|
||||
} from "thirdweb";
|
||||
import { defineChain, getChainMetadata } from "thirdweb/chains";
|
||||
import {
|
||||
type FetchDeployMetadataResult,
|
||||
getContract,
|
||||
getDeployedCloneFactoryContract,
|
||||
resolveContractAbi,
|
||||
} from "thirdweb/contract";
|
||||
import { moduleInstalledEvent } from "thirdweb/modules";
|
||||
import { eth_getCode, getRpcClient } from "thirdweb/rpc";
|
||||
import type { TransactionReceipt } from "thirdweb/transaction";
|
||||
import { type AbiFunction, decodeFunctionData } from "thirdweb/utils";
|
||||
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
|
||||
import { DataTable } from "./data-table";
|
||||
import { NoCrossChainPrompt } from "./no-crosschain-prompt";
|
||||
|
||||
export async function SharedCrossChainPage(props: {
|
||||
contractAddress: string;
|
||||
chainIdOrSlug: string;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
}) {
|
||||
const info = await getContractPageParamsInfo({
|
||||
contractAddress: props.contractAddress,
|
||||
chainIdOrSlug: props.chainIdOrSlug,
|
||||
teamId: props.projectMeta?.teamId,
|
||||
});
|
||||
|
||||
const ProxyDeployedEvent = prepareEvent({
|
||||
signature:
|
||||
"event ProxyDeployedV2(address indexed implementation, address indexed proxy, address indexed deployer, bytes32 inputSalt, bytes data, bytes extraData)",
|
||||
filters: {
|
||||
proxy: props.contractAddress.toLowerCase(),
|
||||
},
|
||||
});
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const { clientContract, serverContract } = info;
|
||||
|
||||
const isModularCore = (await getContractPageMetadata(serverContract))
|
||||
.isModularCore;
|
||||
|
||||
let twCloneFactoryContract: Readonly<
|
||||
ContractOptions<[], `0x${string}`>
|
||||
> | null = null;
|
||||
try {
|
||||
twCloneFactoryContract = await getDeployedCloneFactoryContract({
|
||||
chain: serverContract.chain,
|
||||
client: serverContract.client,
|
||||
});
|
||||
} catch {}
|
||||
|
||||
const originalCode = await eth_getCode(
|
||||
getRpcClient({
|
||||
client: serverContract.client,
|
||||
chain: serverContract.chain,
|
||||
}),
|
||||
{
|
||||
address: serverContract.address,
|
||||
},
|
||||
);
|
||||
|
||||
let initCode: `0x${string}` = "0x";
|
||||
let creationTxReceipt: TransactionReceipt | undefined;
|
||||
let isDirectDeploy = false;
|
||||
try {
|
||||
const res = await fetch(
|
||||
`https://contract.thirdweb-dev.com/creation/${serverContract.chain.id}/${serverContract.address}`,
|
||||
);
|
||||
const creationData = await res.json();
|
||||
|
||||
if (creationData.status === "1" && creationData.result[0]?.txHash) {
|
||||
const rpcClient = getRpcClient({
|
||||
client: serverContract.client,
|
||||
chain: serverContract.chain,
|
||||
});
|
||||
creationTxReceipt = await eth_getTransactionReceipt(rpcClient, {
|
||||
hash: creationData.result[0]?.txHash,
|
||||
});
|
||||
|
||||
const creationTx = await eth_getTransactionByHash(rpcClient, {
|
||||
hash: creationData.result[0]?.txHash,
|
||||
});
|
||||
|
||||
initCode = creationTx.input;
|
||||
isDirectDeploy =
|
||||
creationTx.to?.toLowerCase() ===
|
||||
"0x4e59b44847b379578588920cA78FbF26c0B4956C".toLowerCase();
|
||||
}
|
||||
} catch (e) {
|
||||
console.debug(e);
|
||||
}
|
||||
|
||||
let initializeData: `0x${string}` | undefined;
|
||||
let inputSalt: `0x${string}` | undefined;
|
||||
let creationBlockNumber: bigint | undefined;
|
||||
|
||||
if (twCloneFactoryContract) {
|
||||
const events = await getContractEvents({
|
||||
contract: twCloneFactoryContract,
|
||||
events: [ProxyDeployedEvent],
|
||||
});
|
||||
const event = events.find(
|
||||
(e) => e.args.proxy.toLowerCase() === props.contractAddress.toLowerCase(),
|
||||
);
|
||||
|
||||
initializeData = event?.args.data;
|
||||
inputSalt = event?.args.inputSalt;
|
||||
creationBlockNumber = event?.blockNumber;
|
||||
}
|
||||
|
||||
const defaultChains = [
|
||||
1, 137, 8453, 10, 42161, 11155111, 84532, 11155420, 421614,
|
||||
];
|
||||
|
||||
const chainsDeployedOn = (
|
||||
await Promise.all(
|
||||
defaultChains.map(async (c) => {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
const chain = defineChain(c);
|
||||
|
||||
try {
|
||||
const chainMetadata = await getChainMetadata(chain);
|
||||
|
||||
const rpcRequest = getRpcClient({
|
||||
client: serverContract.client,
|
||||
chain,
|
||||
});
|
||||
const code = await eth_getCode(rpcRequest, {
|
||||
address: props.contractAddress,
|
||||
});
|
||||
|
||||
return {
|
||||
id: chain.id,
|
||||
network: chainMetadata.name,
|
||||
chainId: chain.id,
|
||||
status:
|
||||
code === originalCode
|
||||
? ("DEPLOYED" as const)
|
||||
: ("NOT_DEPLOYED" as const),
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
id: chain.id,
|
||||
network: "",
|
||||
chainId: chain.id,
|
||||
status: "NOT_DEPLOYED" as const,
|
||||
};
|
||||
}
|
||||
}),
|
||||
)
|
||||
).filter((c) => c.chainId !== serverContract.chain.id);
|
||||
|
||||
let coreMetadata: FetchDeployMetadataResult | undefined;
|
||||
try {
|
||||
coreMetadata = (
|
||||
await fetchPublishedContractsFromDeploy({
|
||||
contract: serverContract,
|
||||
client: serverContract.client,
|
||||
})
|
||||
).at(-1) as FetchDeployMetadataResult;
|
||||
} catch {}
|
||||
|
||||
if (!coreMetadata) {
|
||||
return NoCrossChainPrompt();
|
||||
}
|
||||
|
||||
let modulesMetadata: FetchDeployMetadataResult[] | undefined;
|
||||
|
||||
if (isModularCore) {
|
||||
let modules: string[] | undefined;
|
||||
const moduleEvent = moduleInstalledEvent();
|
||||
// extract module address in ModuleInstalled events from transaction receipt
|
||||
if (creationTxReceipt) {
|
||||
const decodedEvent = parseEventLogs({
|
||||
events: [moduleEvent],
|
||||
logs: creationTxReceipt.logs,
|
||||
});
|
||||
|
||||
modules = decodedEvent.map((e) => e.args.installedModule);
|
||||
}
|
||||
|
||||
// fetch events from contract
|
||||
if (!modules && creationBlockNumber) {
|
||||
const events = await getContractEvents({
|
||||
contract: serverContract,
|
||||
events: [moduleEvent],
|
||||
});
|
||||
|
||||
const filteredEvents = events.filter(
|
||||
(e) => e.blockNumber === creationBlockNumber,
|
||||
);
|
||||
|
||||
modules = filteredEvents.map((e) => e.args.installedModule);
|
||||
}
|
||||
|
||||
// if receipt not available, try extracting module address from initialize data
|
||||
if (!modules && initializeData) {
|
||||
// biome-ignore lint/suspicious/noExplicitAny: FIXME
|
||||
const decodedData: any = await decodeFunctionData({
|
||||
contract: serverContract,
|
||||
data: initializeData,
|
||||
});
|
||||
|
||||
const abi = await resolveContractAbi(serverContract).catch(
|
||||
() => undefined,
|
||||
);
|
||||
|
||||
if (abi) {
|
||||
const initializeFunction = abi.find(
|
||||
(i: AbiFunction) => i.type === "function" && i.name === "initialize",
|
||||
) as unknown as AbiFunction;
|
||||
|
||||
const moduleIndex = initializeFunction.inputs.findIndex(
|
||||
(i) => i.name === "_modules" || i.name === "modules",
|
||||
);
|
||||
|
||||
modules = moduleIndex ? decodedData[moduleIndex] : undefined;
|
||||
}
|
||||
}
|
||||
|
||||
modulesMetadata = modules
|
||||
? ((await Promise.all(
|
||||
modules.map(async (m) =>
|
||||
(
|
||||
await fetchPublishedContractsFromDeploy({
|
||||
contract: getContract({
|
||||
chain: serverContract.chain,
|
||||
client: serverContract.client,
|
||||
address: m,
|
||||
}),
|
||||
client: serverContract.client,
|
||||
})
|
||||
).at(-1),
|
||||
),
|
||||
)) as FetchDeployMetadataResult[])
|
||||
: undefined;
|
||||
}
|
||||
|
||||
if (!isDirectDeploy && !initializeData) {
|
||||
return NoCrossChainPrompt();
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<h2 className="mb-1 font-bold text-2xl tracking-tight">
|
||||
Cross-chain contracts
|
||||
</h2>
|
||||
<p className="text-muted-foreground">
|
||||
Deterministically deploy and interact with your contracts on multiple
|
||||
networks.
|
||||
</p>
|
||||
</div>
|
||||
<div className="h-10" />
|
||||
<DataTable
|
||||
coreMetadata={coreMetadata}
|
||||
modulesMetadata={modulesMetadata}
|
||||
initializeData={initializeData}
|
||||
inputSalt={inputSalt}
|
||||
data={chainsDeployedOn}
|
||||
coreContract={clientContract}
|
||||
initCode={initCode}
|
||||
isDirectDeploy={isDirectDeploy}
|
||||
projectMeta={props.projectMeta}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
+29
-9
@@ -33,12 +33,18 @@ import { toast } from "sonner";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import { stringify } from "thirdweb/utils";
|
||||
import { Button, Card, FormLabel, Heading, Text } from "tw-components";
|
||||
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { buildContractPagePath } from "../_utils/contract-page-path";
|
||||
|
||||
interface EventsFeedProps {
|
||||
contract: ThirdwebContract;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
}
|
||||
|
||||
export const EventsFeed: React.FC<EventsFeedProps> = ({ contract }) => {
|
||||
export const EventsFeed: React.FC<EventsFeedProps> = ({
|
||||
contract,
|
||||
projectMeta,
|
||||
}) => {
|
||||
const [autoUpdate, setAutoUpdate] = useState(true);
|
||||
const allEvents = useActivity(contract, autoUpdate);
|
||||
const searchParams = useSearchParams();
|
||||
@@ -81,11 +87,18 @@ export const EventsFeed: React.FC<EventsFeedProps> = ({ contract }) => {
|
||||
value={selectedEvent}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value;
|
||||
|
||||
if (eventTypes.includes(val)) {
|
||||
const path =
|
||||
e.target.value === "all"
|
||||
? `/${chainSlug}/${contract.address}/events`
|
||||
: `/${chainSlug}/${contract.address}/events?event=${val}`;
|
||||
const path = buildContractPagePath({
|
||||
projectMeta,
|
||||
chainIdOrSlug: chainSlug.toString(),
|
||||
contractAddress: contract.address,
|
||||
subpath:
|
||||
e.target.value === "all"
|
||||
? "/events"
|
||||
: `/events?event=${val}`,
|
||||
});
|
||||
|
||||
router.push(path);
|
||||
setSelectedEvent(val);
|
||||
}
|
||||
@@ -154,6 +167,7 @@ export const EventsFeed: React.FC<EventsFeedProps> = ({ contract }) => {
|
||||
setSelectedEvent={setSelectedEvent}
|
||||
contractAddress={contract.address}
|
||||
chainSlug={chainSlug}
|
||||
projectMeta={projectMeta}
|
||||
/>
|
||||
))}
|
||||
</Accordion>
|
||||
@@ -168,6 +182,7 @@ interface EventsFeedItemProps {
|
||||
setSelectedEvent: React.Dispatch<React.SetStateAction<string>>;
|
||||
contractAddress: string;
|
||||
chainSlug: string | number;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
}
|
||||
|
||||
const EventsFeedItem: React.FC<EventsFeedItemProps> = ({
|
||||
@@ -175,9 +190,9 @@ const EventsFeedItem: React.FC<EventsFeedItemProps> = ({
|
||||
setSelectedEvent,
|
||||
contractAddress,
|
||||
chainSlug,
|
||||
projectMeta,
|
||||
}) => {
|
||||
const { onCopy } = useClipboard(transaction.transactionHash);
|
||||
|
||||
const router = useDashboardRouter();
|
||||
|
||||
return (
|
||||
@@ -246,9 +261,14 @@ const EventsFeedItem: React.FC<EventsFeedItemProps> = ({
|
||||
key={idx}
|
||||
onClick={(ev) => {
|
||||
ev.stopPropagation();
|
||||
router.push(
|
||||
`/${chainSlug}/${contractAddress}/events?event=${e.eventName}`,
|
||||
);
|
||||
const path = buildContractPagePath({
|
||||
projectMeta,
|
||||
chainIdOrSlug: chainSlug.toString(),
|
||||
contractAddress: contractAddress,
|
||||
subpath: `/events?event=${e.eventName}`,
|
||||
});
|
||||
|
||||
router.push(path);
|
||||
setSelectedEvent(e.eventName);
|
||||
}}
|
||||
>
|
||||
|
||||
+10
-14
@@ -1,19 +1,15 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { EventsFeed } from "./events-feed";
|
||||
import type { PublicContractPageParams } from "../types";
|
||||
import { SharedEventsPage } from "./shared-events-page";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: Promise<{
|
||||
contractAddress: string;
|
||||
chain_id: string;
|
||||
}>;
|
||||
params: Promise<PublicContractPageParams>;
|
||||
}) {
|
||||
const params = await props.params;
|
||||
const info = await getContractPageParamsInfo(params);
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return <EventsFeed contract={info.clientContract} />;
|
||||
return (
|
||||
<SharedEventsPage
|
||||
contractAddress={params.contractAddress}
|
||||
chainIdOrSlug={params.chain_id}
|
||||
projectMeta={undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { EventsFeed } from "./events-feed";
|
||||
|
||||
export async function SharedEventsPage(props: {
|
||||
contractAddress: string;
|
||||
chainIdOrSlug: string;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
}) {
|
||||
const info = await getContractPageParamsInfo({
|
||||
contractAddress: props.contractAddress,
|
||||
chainIdOrSlug: props.chainIdOrSlug,
|
||||
teamId: props.projectMeta?.teamId,
|
||||
});
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return (
|
||||
<EventsFeed
|
||||
contract={info.clientContract}
|
||||
projectMeta={props.projectMeta}
|
||||
/>
|
||||
);
|
||||
}
|
||||
+7
-35
@@ -1,45 +1,17 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { resolveContractAbi } from "thirdweb/contract";
|
||||
import { getRawAccount } from "../../../../../account/settings/getAccount";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { ContractExplorerPage } from "./ContractExplorerPage";
|
||||
import { ContractExplorerPageClient } from "./ContractExplorerPage.client";
|
||||
import type { PublicContractPageParams } from "../types";
|
||||
import { SharedExplorerPage } from "./shared-explorer-page";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: Promise<{
|
||||
contractAddress: string;
|
||||
chain_id: string;
|
||||
}>;
|
||||
params: Promise<PublicContractPageParams>;
|
||||
}) {
|
||||
const params = await props.params;
|
||||
const info = await getContractPageParamsInfo(params);
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const { clientContract, serverContract, chainMetadata, isLocalhostChain } =
|
||||
info;
|
||||
|
||||
const account = await getRawAccount();
|
||||
|
||||
if (isLocalhostChain) {
|
||||
return (
|
||||
<ContractExplorerPageClient
|
||||
contract={clientContract}
|
||||
chainMetadata={chainMetadata}
|
||||
isLoggedIn={!!account}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const abi = await resolveContractAbi(serverContract).catch(() => undefined);
|
||||
|
||||
return (
|
||||
<ContractExplorerPage
|
||||
contract={clientContract}
|
||||
abi={abi}
|
||||
chainMetadata={chainMetadata}
|
||||
<SharedExplorerPage
|
||||
contractAddress={params.contractAddress}
|
||||
chainIdOrSlug={params.chain_id}
|
||||
projectMeta={undefined}
|
||||
isLoggedIn={!!account}
|
||||
/>
|
||||
);
|
||||
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { resolveContractAbi } from "thirdweb/contract";
|
||||
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { ContractExplorerPage } from "./ContractExplorerPage";
|
||||
import { ContractExplorerPageClient } from "./ContractExplorerPage.client";
|
||||
|
||||
export async function SharedExplorerPage(props: {
|
||||
contractAddress: string;
|
||||
chainIdOrSlug: string;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
isLoggedIn: boolean;
|
||||
}) {
|
||||
const info = await getContractPageParamsInfo({
|
||||
contractAddress: props.contractAddress,
|
||||
chainIdOrSlug: props.chainIdOrSlug,
|
||||
teamId: props.projectMeta?.teamId,
|
||||
});
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const { clientContract, serverContract, chainMetadata, isLocalhostChain } =
|
||||
info;
|
||||
|
||||
if (isLocalhostChain) {
|
||||
return (
|
||||
<ContractExplorerPageClient
|
||||
contract={clientContract}
|
||||
chainMetadata={chainMetadata}
|
||||
isLoggedIn={props.isLoggedIn}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const abi = await resolveContractAbi(serverContract).catch(() => undefined);
|
||||
|
||||
return (
|
||||
<ContractExplorerPage
|
||||
contract={clientContract}
|
||||
abi={abi}
|
||||
chainMetadata={chainMetadata}
|
||||
isLoggedIn={props.isLoggedIn}
|
||||
/>
|
||||
);
|
||||
}
|
||||
+12
-189
@@ -1,26 +1,8 @@
|
||||
import { getProjects } from "@/api/projects";
|
||||
import { getTeams } from "@/api/team";
|
||||
import type { MinimalTeamsAndProjects } from "components/contract-components/contract-deploy-form/add-to-project-card";
|
||||
import { resolveFunctionSelectors } from "lib/selectors";
|
||||
import type { Metadata } from "next";
|
||||
import { notFound } from "next/navigation";
|
||||
import { getContractMetadata } from "thirdweb/extensions/common";
|
||||
import { isAddress, isContractDeployed } from "thirdweb/utils";
|
||||
import { shortenIfAddress } from "utils/usedapp-external";
|
||||
import { NebulaChatButton } from "../../../../../nebula-app/(app)/components/FloatingChat/FloatingChat";
|
||||
import { examplePrompts } from "../../../../../nebula-app/(app)/data/examplePrompts";
|
||||
import {
|
||||
getAuthTokenWalletAddress,
|
||||
getUserThirdwebClient,
|
||||
} from "../../../../api/lib/getAuthToken";
|
||||
import { ConfigureCustomChain } from "./_layout/ConfigureCustomChain";
|
||||
import { getContractMetadataHeaderData } from "./_layout/contract-metadata";
|
||||
import { ContractPageLayout } from "./_layout/contract-page-layout";
|
||||
import { ContractPageLayoutClient } from "./_layout/contract-page-layout.client";
|
||||
import { supportedERCs } from "./_utils/detectedFeatures/supportedERCs";
|
||||
import { getContractPageParamsInfo } from "./_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "./_utils/getContractPageMetadata";
|
||||
import { getContractPageSidebarLinks } from "./_utils/getContractPageSidebarLinks";
|
||||
SharedContractLayout,
|
||||
generateContractLayoutMetadata,
|
||||
} from "./shared-layout";
|
||||
|
||||
export default async function Layout(props: {
|
||||
params: Promise<{
|
||||
@@ -30,130 +12,17 @@ export default async function Layout(props: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const params = await props.params;
|
||||
if (!isAddress(params.contractAddress)) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
const info = await getContractPageParamsInfo(params);
|
||||
|
||||
if (!info) {
|
||||
return <ConfigureCustomChain chainSlug={params.chain_id} />;
|
||||
}
|
||||
|
||||
const { clientContract, serverContract, chainMetadata, isLocalhostChain } =
|
||||
info;
|
||||
|
||||
if (chainMetadata.status === "deprecated") {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const accountAddress = await getAuthTokenWalletAddress();
|
||||
|
||||
const client = await getUserThirdwebClient();
|
||||
const teamsAndProjects = await getTeamsAndProjectsIfLoggedIn();
|
||||
|
||||
if (isLocalhostChain) {
|
||||
return (
|
||||
<ContractPageLayoutClient
|
||||
chainMetadata={chainMetadata}
|
||||
contract={clientContract}
|
||||
teamsAndProjects={teamsAndProjects}
|
||||
client={client}
|
||||
>
|
||||
{props.children}
|
||||
</ContractPageLayoutClient>
|
||||
);
|
||||
}
|
||||
|
||||
// check if the contract exists
|
||||
const isValidContract = await isContractDeployed(serverContract).catch(
|
||||
() => false,
|
||||
);
|
||||
if (!isValidContract) {
|
||||
// TODO - replace 404 with a better page to upsell deploy or other thirdweb products
|
||||
notFound();
|
||||
}
|
||||
const contractPageMetadata = await getContractPageMetadata(serverContract);
|
||||
|
||||
const sidebarLinks = getContractPageSidebarLinks({
|
||||
chainSlug: chainMetadata.slug,
|
||||
contractAddress: serverContract.address,
|
||||
metadata: contractPageMetadata,
|
||||
});
|
||||
|
||||
const { contractMetadata, externalLinks } =
|
||||
await getContractMetadataHeaderData(serverContract);
|
||||
|
||||
const contractAddress = serverContract.address;
|
||||
const chainName = chainMetadata.name;
|
||||
const chainId = chainMetadata.chainId;
|
||||
|
||||
const contractPromptPrefix = `A user is viewing the contract address ${contractAddress} on ${chainName} (Chain ID: ${chainId}). Provide a concise summary of this contract's functionalities, such as token minting, staking, or governance mechanisms. Focus on what the contract enables users to do, avoiding transaction execution details unless requested.
|
||||
Users may be interested in how to interact with the contract. Outline common interaction patterns, such as claiming rewards, participating in governance, or transferring assets. Emphasize the contract's capabilities without guiding through transaction processes unless asked.
|
||||
Provide insights into how the contract is being used. Share information on user engagement, transaction volumes, or integration with other dApps, focusing on the contract's role within the broader ecosystem.
|
||||
Users may be considering integrating the contract into their applications. Discuss how this contract's functionalities can be leveraged within different types of dApps, highlighting potential use cases and benefits.
|
||||
|
||||
The following is the user's message:`;
|
||||
|
||||
return (
|
||||
<ContractPageLayout
|
||||
chainMetadata={chainMetadata}
|
||||
contract={clientContract}
|
||||
sidebarLinks={sidebarLinks}
|
||||
dashboardContractMetadata={contractMetadata}
|
||||
externalLinks={externalLinks}
|
||||
teamsAndProjects={teamsAndProjects}
|
||||
client={client}
|
||||
<SharedContractLayout
|
||||
contractAddress={params.contractAddress}
|
||||
chainIdOrSlug={params.chain_id}
|
||||
projectMeta={undefined}
|
||||
>
|
||||
<NebulaChatButton
|
||||
isLoggedIn={!!accountAddress}
|
||||
networks={info.chainMetadata.testnet ? "testnet" : "mainnet"}
|
||||
isFloating={true}
|
||||
pageType="contract"
|
||||
label="Ask AI about this contract"
|
||||
client={client}
|
||||
nebulaParams={{
|
||||
messagePrefix: contractPromptPrefix,
|
||||
chainIds: [chainId],
|
||||
wallet: accountAddress ?? undefined,
|
||||
}}
|
||||
examplePrompts={examplePrompts}
|
||||
/>
|
||||
{props.children}
|
||||
</ContractPageLayout>
|
||||
</SharedContractLayout>
|
||||
);
|
||||
}
|
||||
|
||||
async function getTeamsAndProjectsIfLoggedIn() {
|
||||
try {
|
||||
const teams = await getTeams();
|
||||
|
||||
if (!teams) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const teamsAndProjects: MinimalTeamsAndProjects = await Promise.all(
|
||||
teams.map(async (team) => ({
|
||||
team: {
|
||||
id: team.id,
|
||||
name: team.name,
|
||||
slug: team.slug,
|
||||
image: team.image,
|
||||
},
|
||||
projects: (await getProjects(team.slug)).map((x) => ({
|
||||
id: x.id,
|
||||
name: x.name,
|
||||
image: x.image,
|
||||
})),
|
||||
})),
|
||||
);
|
||||
|
||||
return teamsAndProjects;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export async function generateMetadata(props: {
|
||||
params: Promise<{
|
||||
contractAddress: string;
|
||||
@@ -161,54 +30,8 @@ export async function generateMetadata(props: {
|
||||
}>;
|
||||
}): Promise<Metadata> {
|
||||
const params = await props.params;
|
||||
try {
|
||||
const info = await getContractPageParamsInfo(params);
|
||||
|
||||
if (!info) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
const [functionSelectors, contractMetadata] = await Promise.all([
|
||||
resolveFunctionSelectors(info.serverContract),
|
||||
getContractMetadata({
|
||||
contract: info.serverContract,
|
||||
}),
|
||||
]);
|
||||
|
||||
if (contractMetadata.name === null) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
const { isERC1155, isERC20, isERC721 } = supportedERCs(functionSelectors);
|
||||
|
||||
const contractDisplayName = `${contractMetadata.name}${
|
||||
contractMetadata.symbol ? ` (${contractMetadata.symbol})` : ""
|
||||
}`;
|
||||
|
||||
const cleanedChainName = info?.chainMetadata?.name
|
||||
.replace("Mainnet", "")
|
||||
.replace("Testnet", "")
|
||||
.trim();
|
||||
|
||||
const title = `${contractDisplayName} | ${cleanedChainName} Smart Contract`;
|
||||
let description = "";
|
||||
|
||||
if (isERC721 || isERC1155) {
|
||||
description = `View tokens, source code, transactions, balances, and analytics for the ${contractDisplayName} smart contract on ${cleanedChainName}.`;
|
||||
} else if (isERC20) {
|
||||
description = `View ERC20 tokens, transactions, balances, source code, and analytics for the ${contractDisplayName} smart contract on ${cleanedChainName}`;
|
||||
} else {
|
||||
description = `View tokens, transactions, balances, source code, and analytics for the ${contractDisplayName} smart contract on ${cleanedChainName}`;
|
||||
}
|
||||
|
||||
return {
|
||||
title: title,
|
||||
description: description,
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
title: `${shortenIfAddress(params.contractAddress)} | ${params.chain_id}`,
|
||||
description: `View tokens, transactions, balances, source code, and analytics for the smart contract on Chain ID ${params.chain_id}`,
|
||||
};
|
||||
}
|
||||
return generateContractLayoutMetadata({
|
||||
contractAddress: params.contractAddress,
|
||||
chainIdOrSlug: params.chain_id,
|
||||
});
|
||||
}
|
||||
|
||||
+8
-1
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { ErrorPage, LoadingPage } from "../_components/page-skeletons";
|
||||
import { RedirectToContractOverview } from "../_components/redirect-contract-overview.client";
|
||||
import { useContractPageMetadata } from "../_hooks/useContractPageMetadata";
|
||||
@@ -9,6 +10,7 @@ import { ContractEditModulesPage } from "./ContractEditModulesPage";
|
||||
export function ContractEditModulesPageClient(props: {
|
||||
contract: ThirdwebContract;
|
||||
isLoggedIn: boolean;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
}) {
|
||||
const metadataQuery = useContractPageMetadata(props.contract);
|
||||
|
||||
@@ -21,7 +23,12 @@ export function ContractEditModulesPageClient(props: {
|
||||
}
|
||||
|
||||
if (!metadataQuery.data.isModularCore) {
|
||||
return <RedirectToContractOverview contract={props.contract} />;
|
||||
return (
|
||||
<RedirectToContractOverview
|
||||
contract={props.contract}
|
||||
projectMeta={props.projectMeta}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
+10
-36
@@ -1,43 +1,17 @@
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { getRawAccount } from "../../../../../account/settings/getAccount";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
|
||||
import { ContractEditModulesPage } from "./ContractEditModulesPage";
|
||||
import { ContractEditModulesPageClient } from "./ContractEditModulesPage.client";
|
||||
import type { PublicContractPageParams } from "../types";
|
||||
import { SharedModulesPage } from "./shared-modules-page";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: Promise<{
|
||||
contractAddress: string;
|
||||
chain_id: string;
|
||||
}>;
|
||||
params: Promise<PublicContractPageParams>;
|
||||
}) {
|
||||
const params = await props.params;
|
||||
const info = await getContractPageParamsInfo(params);
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const { clientContract, serverContract, isLocalhostChain } = info;
|
||||
|
||||
const account = await getRawAccount();
|
||||
|
||||
if (isLocalhostChain) {
|
||||
return (
|
||||
<ContractEditModulesPageClient
|
||||
contract={clientContract}
|
||||
isLoggedIn={!!account}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const { isModularCore } = await getContractPageMetadata(serverContract);
|
||||
|
||||
if (!isModularCore) {
|
||||
redirect(`/${params.chain_id}/${params.contractAddress}`);
|
||||
}
|
||||
|
||||
const [params, account] = await Promise.all([props.params, getRawAccount()]);
|
||||
return (
|
||||
<ContractEditModulesPage contract={clientContract} isLoggedIn={!!account} />
|
||||
<SharedModulesPage
|
||||
contractAddress={params.contractAddress}
|
||||
chainIdOrSlug={params.chain_id}
|
||||
projectMeta={undefined}
|
||||
isLoggedIn={!!account}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { redirectToContractLandingPage } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/utils";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
|
||||
import { ContractEditModulesPage } from "./ContractEditModulesPage";
|
||||
import { ContractEditModulesPageClient } from "./ContractEditModulesPage.client";
|
||||
|
||||
export async function SharedModulesPage(props: {
|
||||
contractAddress: string;
|
||||
chainIdOrSlug: string;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
isLoggedIn: boolean;
|
||||
}) {
|
||||
const info = await getContractPageParamsInfo({
|
||||
contractAddress: props.contractAddress,
|
||||
chainIdOrSlug: props.chainIdOrSlug,
|
||||
teamId: props.projectMeta?.teamId,
|
||||
});
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const { clientContract, serverContract, isLocalhostChain } = info;
|
||||
|
||||
if (isLocalhostChain) {
|
||||
return (
|
||||
<ContractEditModulesPageClient
|
||||
contract={clientContract}
|
||||
isLoggedIn={props.isLoggedIn}
|
||||
projectMeta={props.projectMeta}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const { isModularCore } = await getContractPageMetadata(serverContract);
|
||||
|
||||
if (!isModularCore) {
|
||||
redirectToContractLandingPage({
|
||||
chainIdOrSlug: props.chainIdOrSlug,
|
||||
contractAddress: props.contractAddress,
|
||||
projectMeta: props.projectMeta,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<ContractEditModulesPage
|
||||
contract={clientContract}
|
||||
isLoggedIn={props.isLoggedIn}
|
||||
/>
|
||||
);
|
||||
}
|
||||
+9
-1
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { ErrorPage, LoadingPage } from "../_components/page-skeletons";
|
||||
import { RedirectToContractOverview } from "../_components/redirect-contract-overview.client";
|
||||
import { useContractPageMetadata } from "../_hooks/useContractPageMetadata";
|
||||
@@ -9,6 +10,7 @@ import { ContractNFTPage } from "./ContractNFTPage";
|
||||
export function ContractNFTPageClient(props: {
|
||||
contract: ThirdwebContract;
|
||||
isLoggedIn: boolean;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
}) {
|
||||
const metadataQuery = useContractPageMetadata(props.contract);
|
||||
|
||||
@@ -23,7 +25,12 @@ export function ContractNFTPageClient(props: {
|
||||
const { supportedERCs, functionSelectors } = metadataQuery.data;
|
||||
|
||||
if (!supportedERCs.isERC721 && !supportedERCs.isERC1155) {
|
||||
return <RedirectToContractOverview contract={props.contract} />;
|
||||
return (
|
||||
<RedirectToContractOverview
|
||||
contract={props.contract}
|
||||
projectMeta={props.projectMeta}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -32,6 +39,7 @@ export function ContractNFTPageClient(props: {
|
||||
isErc721={supportedERCs.isERC721}
|
||||
functionSelectors={functionSelectors}
|
||||
isLoggedIn={props.isLoggedIn}
|
||||
projectMeta={props.projectMeta}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+4
@@ -1,6 +1,7 @@
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import * as ERC721Ext from "thirdweb/extensions/erc721";
|
||||
import * as ERC1155Ext from "thirdweb/extensions/erc1155";
|
||||
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { BatchLazyMintButton } from "./components/batch-lazy-mint-button";
|
||||
import { NFTClaimButton } from "./components/claim-button";
|
||||
import { NFTLazyMintButton } from "./components/lazy-mint-button";
|
||||
@@ -16,6 +17,7 @@ interface NftOverviewPageProps {
|
||||
tokenId?: string;
|
||||
functionSelectors: string[];
|
||||
isLoggedIn: boolean;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
}
|
||||
|
||||
export const ContractNFTPage: React.FC<NftOverviewPageProps> = ({
|
||||
@@ -23,6 +25,7 @@ export const ContractNFTPage: React.FC<NftOverviewPageProps> = ({
|
||||
isErc721,
|
||||
functionSelectors,
|
||||
isLoggedIn,
|
||||
projectMeta,
|
||||
}) => {
|
||||
const isERC721ClaimToSupported =
|
||||
ERC721Ext.isClaimToSupported(functionSelectors);
|
||||
@@ -107,6 +110,7 @@ export const ContractNFTPage: React.FC<NftOverviewPageProps> = ({
|
||||
contract={contract}
|
||||
isErc721={isErc721}
|
||||
tokenByIndex={isTokenByIndexSupported}
|
||||
projectMeta={projectMeta}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
+9
-1
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import type { ProjectMeta } from "../../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { ErrorPage, LoadingPage } from "../../_components/page-skeletons";
|
||||
import { RedirectToContractOverview } from "../../_components/redirect-contract-overview.client";
|
||||
import { useContractPageMetadata } from "../../_hooks/useContractPageMetadata";
|
||||
@@ -11,6 +12,7 @@ export function TokenIdPageClient(props: {
|
||||
tokenId: string;
|
||||
isLoggedIn: boolean;
|
||||
accountAddress: string | undefined;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
}) {
|
||||
const { contract } = props;
|
||||
const metadataQuery = useContractPageMetadata(props.contract);
|
||||
@@ -26,7 +28,12 @@ export function TokenIdPageClient(props: {
|
||||
const { supportedERCs } = metadataQuery.data;
|
||||
|
||||
if (!supportedERCs.isERC721 && !supportedERCs.isERC1155) {
|
||||
return <RedirectToContractOverview contract={props.contract} />;
|
||||
return (
|
||||
<RedirectToContractOverview
|
||||
contract={props.contract}
|
||||
projectMeta={props.projectMeta}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -36,6 +43,7 @@ export function TokenIdPageClient(props: {
|
||||
tokenId={props.tokenId}
|
||||
isLoggedIn={props.isLoggedIn}
|
||||
accountAddress={props.accountAddress}
|
||||
projectMeta={props.projectMeta}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+12
-52
@@ -1,63 +1,23 @@
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { getRawAccount } from "../../../../../../account/settings/getAccount";
|
||||
import { getAuthTokenWalletAddress } from "../../../../../../api/lib/getAuthToken";
|
||||
import { getContractPageParamsInfo } from "../../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../../_utils/getContractPageMetadata";
|
||||
import { TokenIdPageClient } from "./TokenIdPage.client";
|
||||
import { TokenIdPage } from "./token-id";
|
||||
import type { PublicContractPageParams } from "../../types";
|
||||
import { SharedNFTTokenPage } from "./shared-nfts-token-page";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: Promise<{
|
||||
contractAddress: string;
|
||||
chain_id: string;
|
||||
tokenId: string;
|
||||
}>;
|
||||
params: Promise<
|
||||
PublicContractPageParams & {
|
||||
tokenId: string;
|
||||
}
|
||||
>;
|
||||
}) {
|
||||
const params = await props.params;
|
||||
const info = await getContractPageParamsInfo(params);
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
if (!isOnlyNumbers(params.tokenId)) {
|
||||
// redirect to nfts index page
|
||||
redirect(`/${params.chain_id}/${params.contractAddress}/nfts`);
|
||||
}
|
||||
|
||||
const [accountAddress, account] = await Promise.all([
|
||||
getAuthTokenWalletAddress(),
|
||||
getRawAccount(),
|
||||
]);
|
||||
|
||||
const { clientContract, serverContract, isLocalhostChain } = info;
|
||||
if (isLocalhostChain) {
|
||||
return (
|
||||
<TokenIdPageClient
|
||||
contract={clientContract}
|
||||
tokenId={params.tokenId}
|
||||
isLoggedIn={!!account}
|
||||
accountAddress={accountAddress || undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const { supportedERCs } = await getContractPageMetadata(serverContract);
|
||||
|
||||
if (!supportedERCs.isERC721 && !supportedERCs.isERC1155) {
|
||||
redirect(`/${params.chain_id}/${params.contractAddress}`);
|
||||
}
|
||||
const [params, account] = await Promise.all([props.params, getRawAccount()]);
|
||||
|
||||
return (
|
||||
<TokenIdPage
|
||||
contract={clientContract}
|
||||
isErc721={supportedERCs.isERC721}
|
||||
<SharedNFTTokenPage
|
||||
contractAddress={params.contractAddress}
|
||||
chainIdOrSlug={params.chain_id}
|
||||
tokenId={params.tokenId}
|
||||
projectMeta={undefined}
|
||||
isLoggedIn={!!account}
|
||||
accountAddress={accountAddress || undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function isOnlyNumbers(str: string) {
|
||||
return /^\d+$/.test(str);
|
||||
}
|
||||
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { getAuthTokenWalletAddress } from "../../../../../../api/lib/getAuthToken";
|
||||
import type { ProjectMeta } from "../../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { redirectToContractLandingPage } from "../../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/utils";
|
||||
import { buildContractPagePath } from "../../_utils/contract-page-path";
|
||||
import { getContractPageParamsInfo } from "../../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../../_utils/getContractPageMetadata";
|
||||
import { TokenIdPageClient } from "./TokenIdPage.client";
|
||||
import { TokenIdPage } from "./token-id";
|
||||
|
||||
export async function SharedNFTTokenPage(props: {
|
||||
contractAddress: string;
|
||||
chainIdOrSlug: string;
|
||||
tokenId: string;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
isLoggedIn: boolean;
|
||||
}) {
|
||||
const [info, accountAddress] = await Promise.all([
|
||||
getContractPageParamsInfo({
|
||||
contractAddress: props.contractAddress,
|
||||
chainIdOrSlug: props.chainIdOrSlug,
|
||||
teamId: props.projectMeta?.teamId,
|
||||
}),
|
||||
getAuthTokenWalletAddress(),
|
||||
]);
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
// redirect to nfts index page
|
||||
if (!isOnlyNumbers(props.tokenId)) {
|
||||
redirect(
|
||||
buildContractPagePath({
|
||||
projectMeta: props.projectMeta,
|
||||
chainIdOrSlug: props.chainIdOrSlug,
|
||||
contractAddress: props.contractAddress,
|
||||
subpath: "/nfts",
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const { clientContract, serverContract, isLocalhostChain } = info;
|
||||
if (isLocalhostChain) {
|
||||
return (
|
||||
<TokenIdPageClient
|
||||
contract={clientContract}
|
||||
tokenId={props.tokenId}
|
||||
isLoggedIn={props.isLoggedIn}
|
||||
accountAddress={accountAddress || undefined}
|
||||
projectMeta={props.projectMeta}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const { supportedERCs } = await getContractPageMetadata(serverContract);
|
||||
|
||||
if (!supportedERCs.isERC721 && !supportedERCs.isERC1155) {
|
||||
redirectToContractLandingPage({
|
||||
chainIdOrSlug: props.chainIdOrSlug,
|
||||
contractAddress: props.contractAddress,
|
||||
projectMeta: props.projectMeta,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<TokenIdPage
|
||||
contract={clientContract}
|
||||
isErc721={supportedERCs.isERC721}
|
||||
tokenId={props.tokenId}
|
||||
isLoggedIn={props.isLoggedIn}
|
||||
accountAddress={accountAddress || undefined}
|
||||
projectMeta={props.projectMeta}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function isOnlyNumbers(str: string) {
|
||||
return /^\d+$/.test(str);
|
||||
}
|
||||
+12
-11
@@ -17,7 +17,6 @@ import { CodeClient } from "@/components/ui/code/code.client";
|
||||
import { TabButtons } from "@/components/ui/tabs";
|
||||
import { ToolTipLabel } from "@/components/ui/tooltip";
|
||||
import { useThirdwebClient } from "@/constants/thirdweb.client";
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
import { resolveSchemeWithErrorHandler } from "@/lib/resolveSchemeWithErrorHandler";
|
||||
import { useChainSlug } from "hooks/chains/chainSlug";
|
||||
import { ExternalLinkIcon } from "lucide-react";
|
||||
@@ -28,6 +27,8 @@ import { getNFT as getErc721NFT } from "thirdweb/extensions/erc721";
|
||||
import { getNFT as getErc1155NFT } from "thirdweb/extensions/erc1155";
|
||||
import { useReadContract } from "thirdweb/react";
|
||||
import { NFTMediaWithEmptyState } from "tw-components/nft-media";
|
||||
import type { ProjectMeta } from "../../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { buildContractPagePath } from "../../_utils/contract-page-path";
|
||||
import { NftProperty } from "../components/nft-property";
|
||||
import { useNFTDrawerTabs } from "./useNftDrawerTabs";
|
||||
|
||||
@@ -53,6 +54,7 @@ interface TokenIdPageProps {
|
||||
isErc721: boolean;
|
||||
isLoggedIn: boolean;
|
||||
accountAddress: string | undefined;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
}
|
||||
|
||||
// TODO: verify the entire nft object with zod schema and display an error message
|
||||
@@ -63,9 +65,9 @@ export const TokenIdPage: React.FC<TokenIdPageProps> = ({
|
||||
isErc721,
|
||||
isLoggedIn,
|
||||
accountAddress,
|
||||
projectMeta,
|
||||
}) => {
|
||||
const [tab, setTab] = useState("Details");
|
||||
const router = useDashboardRouter();
|
||||
const chainId = contract.chain.id;
|
||||
const chainSlug = useChainSlug(chainId || 1);
|
||||
|
||||
@@ -113,6 +115,13 @@ export const TokenIdPage: React.FC<TokenIdPageProps> = ({
|
||||
: undefined,
|
||||
};
|
||||
|
||||
const nftsPagePath = buildContractPagePath({
|
||||
projectMeta,
|
||||
chainIdOrSlug: chainSlug.toString(),
|
||||
contractAddress: contract.address,
|
||||
subpath: "/nfts",
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex flex-col gap-6 lg:flex-row">
|
||||
@@ -129,15 +138,7 @@ export const TokenIdPage: React.FC<TokenIdPageProps> = ({
|
||||
<Breadcrumb className="mb-3">
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink
|
||||
href={`/${chainSlug}/${contract.address}/nfts`}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
router.push(`/${chainSlug}/${contract.address}/nfts`);
|
||||
}}
|
||||
>
|
||||
NFTs
|
||||
</BreadcrumbLink>
|
||||
<BreadcrumbLink href={nftsPagePath}>NFTs</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
|
||||
+15
-6
@@ -41,16 +41,20 @@ import type { NFT, ThirdwebContract } from "thirdweb";
|
||||
import * as ERC721Ext from "thirdweb/extensions/erc721";
|
||||
import * as ERC1155Ext from "thirdweb/extensions/erc1155";
|
||||
import { useReadContract } from "thirdweb/react";
|
||||
import type { ProjectMeta } from "../../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { buildContractPagePath } from "../../_utils/contract-page-path";
|
||||
|
||||
interface ContractOverviewNFTGetAllProps {
|
||||
contract: ThirdwebContract;
|
||||
isErc721: boolean;
|
||||
tokenByIndex: boolean;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
}
|
||||
export const NFTGetAllTable: React.FC<ContractOverviewNFTGetAllProps> = ({
|
||||
contract,
|
||||
isErc721,
|
||||
tokenByIndex,
|
||||
projectMeta,
|
||||
}) => {
|
||||
// if it's not erc721, it's erc1155
|
||||
const isErc1155 = !isErc721;
|
||||
@@ -297,12 +301,17 @@ export const NFTGetAllTable: React.FC<ContractOverviewNFTGetAllProps> = ({
|
||||
if (!tokenId && tokenId !== 0n) {
|
||||
return;
|
||||
}
|
||||
router.push(
|
||||
`/${chainSlug}/${contract.address}/nfts/${tokenId.toString()}`,
|
||||
{
|
||||
scroll: true,
|
||||
},
|
||||
);
|
||||
|
||||
const path = buildContractPagePath({
|
||||
projectMeta,
|
||||
chainIdOrSlug: chainSlug.toString(),
|
||||
contractAddress: contract.address,
|
||||
subpath: `/nfts/${tokenId.toString()}`,
|
||||
});
|
||||
|
||||
router.push(path, {
|
||||
scroll: true,
|
||||
});
|
||||
}}
|
||||
borderBottomWidth={1}
|
||||
_last={{ borderBottomWidth: 0 }}
|
||||
|
||||
+8
-35
@@ -1,44 +1,17 @@
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { getRawAccount } from "../../../../../account/settings/getAccount";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
|
||||
import { ContractNFTPage } from "./ContractNFTPage";
|
||||
import { ContractNFTPageClient } from "./ContractNFTPage.client";
|
||||
import type { PublicContractPageParams } from "../types";
|
||||
import { SharedNFTPage } from "./shared-nfts-page";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: Promise<{
|
||||
contractAddress: string;
|
||||
chain_id: string;
|
||||
}>;
|
||||
params: Promise<PublicContractPageParams>;
|
||||
}) {
|
||||
const params = await props.params;
|
||||
const info = await getContractPageParamsInfo(params);
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const account = await getRawAccount();
|
||||
|
||||
const { clientContract, serverContract, isLocalhostChain } = info;
|
||||
if (isLocalhostChain) {
|
||||
return (
|
||||
<ContractNFTPageClient contract={clientContract} isLoggedIn={!!account} />
|
||||
);
|
||||
}
|
||||
|
||||
const { supportedERCs, functionSelectors } =
|
||||
await getContractPageMetadata(serverContract);
|
||||
|
||||
if (!supportedERCs.isERC721 && !supportedERCs.isERC1155) {
|
||||
redirect(`/${params.chain_id}/${params.contractAddress}`);
|
||||
}
|
||||
const [params, account] = await Promise.all([props.params, getRawAccount()]);
|
||||
|
||||
return (
|
||||
<ContractNFTPage
|
||||
contract={clientContract}
|
||||
isErc721={supportedERCs.isERC721}
|
||||
functionSelectors={functionSelectors}
|
||||
<SharedNFTPage
|
||||
contractAddress={params.contractAddress}
|
||||
chainIdOrSlug={params.chain_id}
|
||||
projectMeta={undefined}
|
||||
isLoggedIn={!!account}
|
||||
/>
|
||||
);
|
||||
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { redirectToContractLandingPage } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/utils";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
|
||||
import { ContractNFTPage } from "./ContractNFTPage";
|
||||
import { ContractNFTPageClient } from "./ContractNFTPage.client";
|
||||
|
||||
export async function SharedNFTPage(props: {
|
||||
contractAddress: string;
|
||||
chainIdOrSlug: string;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
isLoggedIn: boolean;
|
||||
}) {
|
||||
const info = await getContractPageParamsInfo({
|
||||
contractAddress: props.contractAddress,
|
||||
chainIdOrSlug: props.chainIdOrSlug,
|
||||
teamId: props.projectMeta?.teamId,
|
||||
});
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const { clientContract, serverContract, isLocalhostChain } = info;
|
||||
if (isLocalhostChain) {
|
||||
return (
|
||||
<ContractNFTPageClient
|
||||
contract={clientContract}
|
||||
isLoggedIn={props.isLoggedIn}
|
||||
projectMeta={props.projectMeta}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const { supportedERCs, functionSelectors } =
|
||||
await getContractPageMetadata(serverContract);
|
||||
|
||||
if (!supportedERCs.isERC721 && !supportedERCs.isERC1155) {
|
||||
redirectToContractLandingPage({
|
||||
chainIdOrSlug: props.chainIdOrSlug,
|
||||
contractAddress: props.contractAddress,
|
||||
projectMeta: props.projectMeta,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<ContractNFTPage
|
||||
contract={clientContract}
|
||||
isErc721={supportedERCs.isERC721}
|
||||
functionSelectors={functionSelectors}
|
||||
isLoggedIn={props.isLoggedIn}
|
||||
projectMeta={props.projectMeta}
|
||||
/>
|
||||
);
|
||||
}
|
||||
+5
-1
@@ -21,7 +21,11 @@ export default async function Image({
|
||||
};
|
||||
}) {
|
||||
try {
|
||||
const info = await getContractPageParamsInfo(params);
|
||||
const info = await getContractPageParamsInfo({
|
||||
contractAddress: params.contractAddress,
|
||||
chainIdOrSlug: params.chain_id,
|
||||
teamId: undefined,
|
||||
});
|
||||
|
||||
if (!info) {
|
||||
throw new Error();
|
||||
|
||||
+10
@@ -1,4 +1,5 @@
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { TokenDetailsCard } from "../tokens/components/supply";
|
||||
import { ContractAnalyticsOverviewCard } from "./components/Analytics";
|
||||
import { BuildYourApp } from "./components/BuildYourApp";
|
||||
@@ -20,6 +21,7 @@ interface ContractOverviewPageProps {
|
||||
isAnalyticsSupported: boolean;
|
||||
functionSelectors: string[];
|
||||
publishedBy: React.ReactNode;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
}
|
||||
|
||||
const TRACKING_CATEGORY = "contract_overview";
|
||||
@@ -36,6 +38,7 @@ export const ContractOverviewPage: React.FC<ContractOverviewPageProps> = ({
|
||||
isAnalyticsSupported,
|
||||
functionSelectors,
|
||||
publishedBy,
|
||||
projectMeta,
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex flex-col gap-10 lg:flex-row lg:gap-8">
|
||||
@@ -47,6 +50,7 @@ export const ContractOverviewPage: React.FC<ContractOverviewPageProps> = ({
|
||||
contract={contract}
|
||||
chainSlug={chainSlug}
|
||||
functionSelectors={functionSelectors}
|
||||
projectMeta={projectMeta}
|
||||
/>
|
||||
|
||||
{isAnalyticsSupported && (
|
||||
@@ -55,12 +59,14 @@ export const ContractOverviewPage: React.FC<ContractOverviewPageProps> = ({
|
||||
chainId={contract.chain.id}
|
||||
trackingCategory={TRACKING_CATEGORY}
|
||||
chainSlug={chainSlug}
|
||||
projectMeta={projectMeta}
|
||||
/>
|
||||
)}
|
||||
|
||||
{(hasEnglishAuctions || hasDirectListings) && (
|
||||
<MarketplaceDetails
|
||||
contract={contract}
|
||||
projectMeta={projectMeta}
|
||||
trackingCategory={TRACKING_CATEGORY}
|
||||
hasEnglishAuctions={hasEnglishAuctions}
|
||||
hasDirectListings={hasDirectListings}
|
||||
@@ -74,6 +80,7 @@ export const ContractOverviewPage: React.FC<ContractOverviewPageProps> = ({
|
||||
trackingCategory={TRACKING_CATEGORY}
|
||||
isErc721={isErc721}
|
||||
chainSlug={chainSlug}
|
||||
projectMeta={projectMeta}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -83,6 +90,7 @@ export const ContractOverviewPage: React.FC<ContractOverviewPageProps> = ({
|
||||
contract={contract}
|
||||
trackingCategory={TRACKING_CATEGORY}
|
||||
chainSlug={chainSlug}
|
||||
projectMeta={projectMeta}
|
||||
/>
|
||||
|
||||
{isPermissionsEnumerable && (
|
||||
@@ -90,6 +98,7 @@ export const ContractOverviewPage: React.FC<ContractOverviewPageProps> = ({
|
||||
contract={contract}
|
||||
trackingCategory={TRACKING_CATEGORY}
|
||||
chainSlug={chainSlug}
|
||||
projectMeta={projectMeta}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -97,6 +106,7 @@ export const ContractOverviewPage: React.FC<ContractOverviewPageProps> = ({
|
||||
trackingCategory={TRACKING_CATEGORY}
|
||||
chainSlug={chainSlug}
|
||||
contractAddress={contract.address}
|
||||
projectMeta={projectMeta}
|
||||
/>
|
||||
</div>
|
||||
<div className="shrink-0 lg:w-[300px]">{publishedBy}</div>
|
||||
|
||||
+11
-3
@@ -11,6 +11,8 @@ import { useTrack } from "hooks/analytics/useTrack";
|
||||
import { ArrowRightIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useMemo, useState } from "react";
|
||||
import type { ProjectMeta } from "../../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { buildContractPagePath } from "../../_utils/contract-page-path";
|
||||
|
||||
function getDayKey(date: Date) {
|
||||
return date.toISOString().split("T")[0];
|
||||
@@ -21,6 +23,7 @@ export function ContractAnalyticsOverviewCard(props: {
|
||||
chainId: number;
|
||||
trackingCategory: string;
|
||||
chainSlug: string;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
}) {
|
||||
const trackEvent = useTrack();
|
||||
const [startDate] = useState(
|
||||
@@ -85,6 +88,13 @@ export function ContractAnalyticsOverviewCard(props: {
|
||||
});
|
||||
}, [wallets.data, transactions.data, events.data, isPending]);
|
||||
|
||||
const analyticsPath = buildContractPagePath({
|
||||
projectMeta: props.projectMeta,
|
||||
chainIdOrSlug: props.chainSlug,
|
||||
contractAddress: props.contractAddress,
|
||||
subpath: "/analytics",
|
||||
});
|
||||
|
||||
return (
|
||||
<ThirdwebAreaChart
|
||||
config={{
|
||||
@@ -121,9 +131,7 @@ export function ContractAnalyticsOverviewCard(props: {
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Link
|
||||
href={`/${props.chainSlug}/${props.contractAddress}/analytics`}
|
||||
>
|
||||
<Link href={analyticsPath}>
|
||||
<span>View All</span>
|
||||
<ArrowRightIcon className="size-4" />
|
||||
</Link>
|
||||
|
||||
+11
-1
@@ -1,12 +1,22 @@
|
||||
import { TrackedLinkTW } from "@/components/ui/tracked-link";
|
||||
import { ReactIcon } from "components/icons/brand-icons/ReactIcon";
|
||||
import { TypeScriptIcon } from "components/icons/brand-icons/TypeScriptIcon";
|
||||
import type { ProjectMeta } from "../../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { buildContractPagePath } from "../../_utils/contract-page-path";
|
||||
|
||||
export function BuildYourApp(props: {
|
||||
trackingCategory: string;
|
||||
contractAddress: string;
|
||||
chainSlug: string;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
}) {
|
||||
const codePath = buildContractPagePath({
|
||||
projectMeta: props.projectMeta,
|
||||
chainIdOrSlug: props.chainSlug,
|
||||
contractAddress: props.contractAddress,
|
||||
subpath: "/code",
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="relative flex flex-col justify-between gap-4 rounded-lg border bg-card p-6 hover:border-active-border lg:flex-row">
|
||||
{/* left */}
|
||||
@@ -17,7 +27,7 @@ export function BuildYourApp(props: {
|
||||
<TrackedLinkTW
|
||||
category={props.trackingCategory}
|
||||
label="build_your_app"
|
||||
href={`/${props.chainSlug}/${props.contractAddress}/code`}
|
||||
href={codePath}
|
||||
className="block text-balance text-muted-foreground text-sm before:absolute before:inset-0"
|
||||
>
|
||||
Learn more about how you can use thirdweb tools to build apps on top
|
||||
|
||||
+14
-4
@@ -12,6 +12,8 @@ import * as ERC1155Ext from "thirdweb/extensions/erc1155";
|
||||
import * as ERC4337Ext from "thirdweb/extensions/erc4337";
|
||||
import { getAccounts } from "thirdweb/extensions/erc4337";
|
||||
import { useReadContract } from "thirdweb/react";
|
||||
import type { ProjectMeta } from "../../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { buildContractPagePath } from "../../_utils/contract-page-path";
|
||||
|
||||
interface ContractChecklistProps {
|
||||
contract: ThirdwebContract;
|
||||
@@ -20,6 +22,7 @@ interface ContractChecklistProps {
|
||||
isErc20: boolean;
|
||||
chainSlug: string;
|
||||
functionSelectors: string[];
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
}
|
||||
|
||||
type Step = {
|
||||
@@ -44,13 +47,20 @@ function Inner({
|
||||
isErc721,
|
||||
functionSelectors,
|
||||
chainSlug,
|
||||
projectMeta,
|
||||
}: ContractChecklistProps & {
|
||||
functionSelectors: string[];
|
||||
}) {
|
||||
const nftHref = `/${chainSlug}/${contract.address}/nfts`;
|
||||
const tokenHref = `/${chainSlug}/${contract.address}/tokens`;
|
||||
const accountsHref = `/${chainSlug}/${contract.address}/accounts`;
|
||||
const claimConditionsHref = `/${chainSlug}/${contract.address}/claim-conditions`;
|
||||
const contractLayout = buildContractPagePath({
|
||||
projectMeta,
|
||||
chainIdOrSlug: chainSlug,
|
||||
contractAddress: contract.address,
|
||||
});
|
||||
|
||||
const nftHref = `${contractLayout}/nfts`;
|
||||
const tokenHref = `${contractLayout}/tokens`;
|
||||
const accountsHref = `${contractLayout}/accounts`;
|
||||
const claimConditionsHref = `${contractLayout}/claim-conditions`;
|
||||
|
||||
const erc721Claimed = useReadContract(ERC721Ext.getTotalClaimedSupply, {
|
||||
contract: contract,
|
||||
|
||||
+11
-1
@@ -22,20 +22,30 @@ import { ArrowRightIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import { shortenString } from "utils/usedapp-external";
|
||||
import type { ProjectMeta } from "../../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { buildContractPagePath } from "../../_utils/contract-page-path";
|
||||
|
||||
export function LatestEvents(props: {
|
||||
trackingCategory: string;
|
||||
contract: ThirdwebContract;
|
||||
chainSlug: string;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
}) {
|
||||
const autoUpdate = true;
|
||||
const allEvents = useActivity(props.contract, autoUpdate);
|
||||
|
||||
const eventsHref = buildContractPagePath({
|
||||
projectMeta: props.projectMeta,
|
||||
chainIdOrSlug: props.chainSlug,
|
||||
contractAddress: props.contract.address,
|
||||
subpath: "/events",
|
||||
});
|
||||
|
||||
return (
|
||||
<LatestEventsUI
|
||||
allEvents={allEvents}
|
||||
autoUpdate={autoUpdate}
|
||||
eventsHref={`/${props.chainSlug}/${props.contract.address}/events`}
|
||||
eventsHref={eventsHref}
|
||||
trackingCategory={props.trackingCategory}
|
||||
/>
|
||||
);
|
||||
|
||||
+35
-4
@@ -19,6 +19,8 @@ import {
|
||||
import { useReadContract } from "thirdweb/react";
|
||||
import { min } from "thirdweb/utils";
|
||||
import { NFTMediaWithEmptyState } from "tw-components/nft-media";
|
||||
import type { ProjectMeta } from "../../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { buildContractPagePath } from "../../_utils/contract-page-path";
|
||||
import { ListingStatsV3 } from "./listing-stats";
|
||||
|
||||
type ListingData =
|
||||
@@ -41,14 +43,22 @@ type ListingCardsSectionProps = {
|
||||
contract: ThirdwebContract;
|
||||
trackingCategory: string;
|
||||
chainSlug: string;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
};
|
||||
|
||||
const DirectListingCards: React.FC<ListingCardsSectionProps> = ({
|
||||
trackingCategory,
|
||||
contract,
|
||||
chainSlug,
|
||||
projectMeta,
|
||||
}) => {
|
||||
const directListingsHref = `/${chainSlug}/${contract.address}/direct-listings`;
|
||||
const directListingsHref = buildContractPagePath({
|
||||
projectMeta,
|
||||
chainIdOrSlug: chainSlug,
|
||||
contractAddress: contract.address,
|
||||
subpath: "/direct-listings",
|
||||
});
|
||||
|
||||
const countQuery = useReadContract(totalListings, { contract });
|
||||
const listingsQuery = useReadContract(getAllListings, {
|
||||
contract,
|
||||
@@ -109,6 +119,7 @@ const DirectListingCards: React.FC<ListingCardsSectionProps> = ({
|
||||
trackingCategory={trackingCategory}
|
||||
chainSlug={chainSlug}
|
||||
contractAddress={contract.address}
|
||||
projectMeta={projectMeta}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -119,8 +130,15 @@ const EnglishAuctionCards: React.FC<ListingCardsSectionProps> = ({
|
||||
trackingCategory,
|
||||
contract,
|
||||
chainSlug,
|
||||
projectMeta,
|
||||
}) => {
|
||||
const englishAuctionsHref = `/${chainSlug}/${contract.address}/english-auctions`;
|
||||
const englishAuctionsHref = buildContractPagePath({
|
||||
projectMeta,
|
||||
chainIdOrSlug: chainSlug,
|
||||
contractAddress: contract.address,
|
||||
subpath: "/english-auctions",
|
||||
});
|
||||
|
||||
const countQuery = useReadContract(totalAuctions, { contract });
|
||||
const auctionsQuery = useReadContract(getAllAuctions, {
|
||||
contract,
|
||||
@@ -180,6 +198,7 @@ const EnglishAuctionCards: React.FC<ListingCardsSectionProps> = ({
|
||||
trackingCategory={trackingCategory}
|
||||
chainSlug={chainSlug}
|
||||
contractAddress={contract.address}
|
||||
projectMeta={projectMeta}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -192,6 +211,7 @@ interface MarketplaceDetailsVersionProps {
|
||||
hasEnglishAuctions: boolean;
|
||||
hasDirectListings: boolean;
|
||||
chainSlug: string;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
}
|
||||
|
||||
export const MarketplaceDetails: React.FC<MarketplaceDetailsVersionProps> = ({
|
||||
@@ -200,6 +220,7 @@ export const MarketplaceDetails: React.FC<MarketplaceDetailsVersionProps> = ({
|
||||
hasDirectListings,
|
||||
hasEnglishAuctions,
|
||||
chainSlug,
|
||||
projectMeta,
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex flex-col gap-10">
|
||||
@@ -214,6 +235,7 @@ export const MarketplaceDetails: React.FC<MarketplaceDetailsVersionProps> = ({
|
||||
contract={contract}
|
||||
trackingCategory={trackingCategory}
|
||||
chainSlug={chainSlug}
|
||||
projectMeta={projectMeta}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -222,6 +244,7 @@ export const MarketplaceDetails: React.FC<MarketplaceDetailsVersionProps> = ({
|
||||
contract={contract}
|
||||
trackingCategory={trackingCategory}
|
||||
chainSlug={chainSlug}
|
||||
projectMeta={projectMeta}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -269,6 +292,7 @@ interface ListingCardsProps {
|
||||
isMarketplaceV1?: boolean;
|
||||
chainSlug: string;
|
||||
contractAddress: string;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
}
|
||||
const ListingCards: React.FC<ListingCardsProps> = ({
|
||||
listings,
|
||||
@@ -277,13 +301,20 @@ const ListingCards: React.FC<ListingCardsProps> = ({
|
||||
trackingCategory,
|
||||
chainSlug,
|
||||
contractAddress,
|
||||
projectMeta,
|
||||
}) => {
|
||||
const contractLayout = buildContractPagePath({
|
||||
projectMeta,
|
||||
chainIdOrSlug: chainSlug,
|
||||
contractAddress,
|
||||
});
|
||||
|
||||
listings = isPending
|
||||
? Array.from({ length: 3 }).map((_, idx) => dummyMetadata(idx))
|
||||
: listings.slice(0, 3);
|
||||
|
||||
const directListingsHref = `/${chainSlug}/${contractAddress}/direct-listings`;
|
||||
const englishAuctionsHref = `/${chainSlug}/${contractAddress}/english-auctions`;
|
||||
const directListingsHref = `${contractLayout}/direct-listings`;
|
||||
const englishAuctionsHref = `${contractLayout}/english-auctions`;
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-3 ">
|
||||
|
||||
+11
-1
@@ -7,13 +7,16 @@ import type { ThirdwebContract } from "thirdweb";
|
||||
import * as ERC721 from "thirdweb/extensions/erc721";
|
||||
import * as ERC1155 from "thirdweb/extensions/erc1155";
|
||||
import { useReadContract } from "thirdweb/react";
|
||||
import type { ProjectMeta } from "../../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { NFTCards } from "../../_components/NFTCards";
|
||||
import { buildContractPagePath } from "../../_utils/contract-page-path";
|
||||
|
||||
type NFTDetailsProps = {
|
||||
contract: ThirdwebContract;
|
||||
trackingCategory: string;
|
||||
isErc721: boolean;
|
||||
chainSlug: string;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
};
|
||||
|
||||
export function NFTDetails({
|
||||
@@ -21,8 +24,14 @@ export function NFTDetails({
|
||||
trackingCategory,
|
||||
isErc721,
|
||||
chainSlug,
|
||||
projectMeta,
|
||||
}: NFTDetailsProps) {
|
||||
const nftsHref = `/${chainSlug}/${contract.address}/nfts`;
|
||||
const nftsHref = buildContractPagePath({
|
||||
projectMeta,
|
||||
chainIdOrSlug: chainSlug,
|
||||
contractAddress: contract.address,
|
||||
subpath: "/nfts",
|
||||
});
|
||||
|
||||
const nftQuery = useReadContract(
|
||||
isErc721 ? ERC721.getNFTs : ERC1155.getNFTs,
|
||||
@@ -58,6 +67,7 @@ export function NFTDetails({
|
||||
{/* cards */}
|
||||
<div className="p-6">
|
||||
<NFTCards
|
||||
projectMeta={projectMeta}
|
||||
nfts={displayableNFTs.map((t) => ({
|
||||
...t,
|
||||
contractAddress: contract.address,
|
||||
|
||||
+11
-1
@@ -19,12 +19,22 @@ import { ArrowRightIcon } from "lucide-react";
|
||||
import { useMemo } from "react";
|
||||
import { type ThirdwebContract, ZERO_ADDRESS } from "thirdweb";
|
||||
import { useReadContract } from "thirdweb/react";
|
||||
import type { ProjectMeta } from "../../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { buildContractPagePath } from "../../_utils/contract-page-path";
|
||||
|
||||
export function PermissionsTable(props: {
|
||||
contract: ThirdwebContract;
|
||||
trackingCategory: string;
|
||||
chainSlug: string;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
}) {
|
||||
const permissionsHref = buildContractPagePath({
|
||||
projectMeta: props.projectMeta,
|
||||
chainIdOrSlug: props.chainSlug,
|
||||
contractAddress: props.contract.address,
|
||||
subpath: "/permissions",
|
||||
});
|
||||
|
||||
const allRoleMembers = useReadContract(getAllRoleMembers, {
|
||||
contract: props.contract,
|
||||
});
|
||||
@@ -53,7 +63,7 @@ export function PermissionsTable(props: {
|
||||
<PermissionsTableUI
|
||||
members={members}
|
||||
isPending={allRoleMembers.isPending}
|
||||
viewMoreLink={`/${props.chainSlug}/${props.contract.address}/permissions`}
|
||||
viewMoreLink={permissionsHref}
|
||||
trackingCategory={props.trackingCategory}
|
||||
/>
|
||||
);
|
||||
|
||||
+4
-1
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import type { ChainMetadata } from "thirdweb/chains";
|
||||
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { ErrorPage, LoadingPage } from "../_components/page-skeletons";
|
||||
import { useContractPageMetadata } from "../_hooks/useContractPageMetadata";
|
||||
import { ContractOverviewPage } from "./ContractOverviewPage";
|
||||
@@ -8,8 +9,9 @@ import { ContractOverviewPage } from "./ContractOverviewPage";
|
||||
export function ContractOverviewPageClient(props: {
|
||||
contract: ThirdwebContract;
|
||||
chainMetadata: ChainMetadata;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
}) {
|
||||
const { contract, chainMetadata } = props;
|
||||
const { contract, chainMetadata, projectMeta } = props;
|
||||
const metadataQuery = useContractPageMetadata(contract);
|
||||
|
||||
if (metadataQuery.isPending) {
|
||||
@@ -25,6 +27,7 @@ export function ContractOverviewPageClient(props: {
|
||||
return (
|
||||
<ContractOverviewPage
|
||||
contract={contract}
|
||||
projectMeta={projectMeta}
|
||||
hasDirectListings={contractPageMetadata.isDirectListingSupported}
|
||||
hasEnglishAuctions={contractPageMetadata.isEnglishAuctionSupported}
|
||||
isErc1155={contractPageMetadata.supportedERCs.isERC1155}
|
||||
|
||||
+7
-48
@@ -1,56 +1,15 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { ErrorBoundary } from "react-error-boundary";
|
||||
import { getContractPageParamsInfo } from "./_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "./_utils/getContractPageMetadata";
|
||||
import { ContractOverviewPage } from "./overview/ContractOverviewPage";
|
||||
import { PublishedBy } from "./overview/components/published-by.server";
|
||||
import { ContractOverviewPageClient } from "./overview/contract-overview-page.client";
|
||||
import { SharedContractOverviewPage } from "./shared-overview-page";
|
||||
import type { PublicContractPageParams } from "./types";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: Promise<{
|
||||
contractAddress: string;
|
||||
chain_id: string;
|
||||
}>;
|
||||
params: Promise<PublicContractPageParams>;
|
||||
}) {
|
||||
const params = await props.params;
|
||||
const info = await getContractPageParamsInfo(params);
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const { clientContract, serverContract, chainMetadata, isLocalhostChain } =
|
||||
info;
|
||||
if (isLocalhostChain) {
|
||||
return (
|
||||
<ContractOverviewPageClient
|
||||
chainMetadata={chainMetadata}
|
||||
contract={clientContract}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const contractPageMetadata = await getContractPageMetadata(serverContract);
|
||||
|
||||
return (
|
||||
<ContractOverviewPage
|
||||
contract={clientContract}
|
||||
hasDirectListings={contractPageMetadata.isDirectListingSupported}
|
||||
hasEnglishAuctions={contractPageMetadata.isEnglishAuctionSupported}
|
||||
isErc1155={contractPageMetadata.supportedERCs.isERC1155}
|
||||
isErc20={contractPageMetadata.supportedERCs.isERC20}
|
||||
isErc721={contractPageMetadata.supportedERCs.isERC721}
|
||||
isPermissionsEnumerable={
|
||||
contractPageMetadata.isPermissionsEnumerableSupported
|
||||
}
|
||||
chainSlug={chainMetadata.slug}
|
||||
isAnalyticsSupported={contractPageMetadata.isInsightSupported}
|
||||
functionSelectors={contractPageMetadata.functionSelectors}
|
||||
publishedBy={
|
||||
<ErrorBoundary fallback={null}>
|
||||
<PublishedBy contract={serverContract} />
|
||||
</ErrorBoundary>
|
||||
}
|
||||
<SharedContractOverviewPage
|
||||
contractAddress={params.contractAddress}
|
||||
chainIdOrSlug={params.chain_id}
|
||||
projectMeta={undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+3
@@ -2,6 +2,7 @@
|
||||
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import type { ChainMetadata } from "thirdweb/chains";
|
||||
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { ErrorPage, LoadingPage } from "../_components/page-skeletons";
|
||||
import { useContractPageMetadata } from "../_hooks/useContractPageMetadata";
|
||||
import { ContractPermissionsPage } from "./ContractPermissionsPage";
|
||||
@@ -10,6 +11,7 @@ export function ContractPermissionsPageClient(props: {
|
||||
contract: ThirdwebContract;
|
||||
chainMetadata: ChainMetadata;
|
||||
isLoggedIn: boolean;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
}) {
|
||||
const metadataQuery = useContractPageMetadata(props.contract);
|
||||
|
||||
@@ -23,6 +25,7 @@ export function ContractPermissionsPageClient(props: {
|
||||
|
||||
return (
|
||||
<ContractPermissionsPage
|
||||
projectMeta={props.projectMeta}
|
||||
contract={props.contract}
|
||||
chainSlug={props.chainMetadata.slug}
|
||||
isLoggedIn={props.isLoggedIn}
|
||||
|
||||
+16
-7
@@ -1,10 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import { InlineCode } from "@/components/ui/inline-code";
|
||||
import { useIsomorphicLayoutEffect } from "@/lib/useIsomorphicLayoutEffect";
|
||||
import { ButtonGroup, Divider, Flex } from "@chakra-ui/react";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import { Card, Heading, Link, LinkButton, Text } from "tw-components";
|
||||
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { buildContractPagePath } from "../_utils/contract-page-path";
|
||||
import { Permissions } from "./components";
|
||||
|
||||
interface ContractPermissionsPageProps {
|
||||
@@ -12,16 +13,24 @@ interface ContractPermissionsPageProps {
|
||||
detectedPermissionEnumerable: boolean;
|
||||
chainSlug: string;
|
||||
isLoggedIn: boolean;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
}
|
||||
|
||||
export const ContractPermissionsPage: React.FC<
|
||||
ContractPermissionsPageProps
|
||||
> = ({ contract, detectedPermissionEnumerable, chainSlug, isLoggedIn }) => {
|
||||
useIsomorphicLayoutEffect(() => {
|
||||
window?.scrollTo({ top: 0, behavior: "smooth" });
|
||||
}, []);
|
||||
|
||||
const explorerHref = `/${chainSlug}/${contract.address}/explorer`;
|
||||
> = ({
|
||||
contract,
|
||||
detectedPermissionEnumerable,
|
||||
chainSlug,
|
||||
isLoggedIn,
|
||||
projectMeta,
|
||||
}) => {
|
||||
const explorerHref = buildContractPagePath({
|
||||
projectMeta,
|
||||
chainIdOrSlug: chainSlug,
|
||||
contractAddress: contract.address,
|
||||
subpath: "/explorer",
|
||||
});
|
||||
|
||||
if (!detectedPermissionEnumerable) {
|
||||
return (
|
||||
|
||||
+8
-36
@@ -1,44 +1,16 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { getRawAccount } from "../../../../../account/settings/getAccount";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
|
||||
import { ContractPermissionsPage } from "./ContractPermissionsPage";
|
||||
import { ContractPermissionsPageClient } from "./ContractPermissionsPage.client";
|
||||
import type { PublicContractPageParams } from "../types";
|
||||
import { SharedPermissionsPage } from "./shared-permissions-page";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: Promise<{
|
||||
contractAddress: string;
|
||||
chain_id: string;
|
||||
}>;
|
||||
params: Promise<PublicContractPageParams>;
|
||||
}) {
|
||||
const params = await props.params;
|
||||
const info = await getContractPageParamsInfo(params);
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const account = await getRawAccount();
|
||||
|
||||
const { clientContract, serverContract, isLocalhostChain } = info;
|
||||
if (isLocalhostChain) {
|
||||
return (
|
||||
<ContractPermissionsPageClient
|
||||
contract={clientContract}
|
||||
chainMetadata={info.chainMetadata}
|
||||
isLoggedIn={!!account}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const { isPermissionsEnumerableSupported } =
|
||||
await getContractPageMetadata(serverContract);
|
||||
|
||||
const [params, account] = await Promise.all([props.params, getRawAccount()]);
|
||||
return (
|
||||
<ContractPermissionsPage
|
||||
contract={clientContract}
|
||||
chainSlug={info.chainMetadata.slug}
|
||||
detectedPermissionEnumerable={isPermissionsEnumerableSupported}
|
||||
<SharedPermissionsPage
|
||||
contractAddress={params.contractAddress}
|
||||
chainIdOrSlug={params.chain_id}
|
||||
projectMeta={undefined}
|
||||
isLoggedIn={!!account}
|
||||
/>
|
||||
);
|
||||
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
|
||||
import { ContractPermissionsPage } from "./ContractPermissionsPage";
|
||||
import { ContractPermissionsPageClient } from "./ContractPermissionsPage.client";
|
||||
|
||||
export async function SharedPermissionsPage(props: {
|
||||
contractAddress: string;
|
||||
chainIdOrSlug: string;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
isLoggedIn: boolean;
|
||||
}) {
|
||||
const info = await getContractPageParamsInfo({
|
||||
contractAddress: props.contractAddress,
|
||||
chainIdOrSlug: props.chainIdOrSlug,
|
||||
teamId: props.projectMeta?.teamId,
|
||||
});
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const { clientContract, serverContract, isLocalhostChain } = info;
|
||||
if (isLocalhostChain) {
|
||||
return (
|
||||
<ContractPermissionsPageClient
|
||||
contract={clientContract}
|
||||
chainMetadata={info.chainMetadata}
|
||||
isLoggedIn={props.isLoggedIn}
|
||||
projectMeta={props.projectMeta}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const { isPermissionsEnumerableSupported } =
|
||||
await getContractPageMetadata(serverContract);
|
||||
|
||||
return (
|
||||
<ContractPermissionsPage
|
||||
projectMeta={props.projectMeta}
|
||||
contract={clientContract}
|
||||
chainSlug={info.chainMetadata.slug}
|
||||
detectedPermissionEnumerable={isPermissionsEnumerableSupported}
|
||||
isLoggedIn={props.isLoggedIn}
|
||||
/>
|
||||
);
|
||||
}
|
||||
+8
-1
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { ErrorPage, LoadingPage } from "../_components/page-skeletons";
|
||||
import { RedirectToContractOverview } from "../_components/redirect-contract-overview.client";
|
||||
import { useContractPageMetadata } from "../_hooks/useContractPageMetadata";
|
||||
@@ -9,6 +10,7 @@ import { ContractProposalsPage } from "./ContractProposalsPage";
|
||||
export function ContractProposalsPageClient(props: {
|
||||
contract: ThirdwebContract;
|
||||
isLoggedIn: boolean;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
}) {
|
||||
const metadataQuery = useContractPageMetadata(props.contract);
|
||||
|
||||
@@ -21,7 +23,12 @@ export function ContractProposalsPageClient(props: {
|
||||
}
|
||||
|
||||
if (!metadataQuery.data.isVoteContract) {
|
||||
return <RedirectToContractOverview contract={props.contract} />;
|
||||
return (
|
||||
<RedirectToContractOverview
|
||||
contract={props.contract}
|
||||
projectMeta={props.projectMeta}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
+10
-33
@@ -1,41 +1,18 @@
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { getRawAccount } from "../../../../../account/settings/getAccount";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
|
||||
import { ContractProposalsPage } from "./ContractProposalsPage";
|
||||
import { ContractProposalsPageClient } from "./ContractProposalsPage.client";
|
||||
import type { PublicContractPageParams } from "../types";
|
||||
import { SharedContractProposalsPage } from "./shared-proposals-page";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: Promise<{
|
||||
contractAddress: string;
|
||||
chain_id: string;
|
||||
}>;
|
||||
params: Promise<PublicContractPageParams>;
|
||||
}) {
|
||||
const params = await props.params;
|
||||
const info = await getContractPageParamsInfo(params);
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
const account = await getRawAccount();
|
||||
|
||||
const { clientContract, serverContract, isLocalhostChain } = info;
|
||||
if (isLocalhostChain) {
|
||||
return (
|
||||
<ContractProposalsPageClient
|
||||
contract={clientContract}
|
||||
isLoggedIn={!!account}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const { isVoteContract } = await getContractPageMetadata(serverContract);
|
||||
|
||||
if (!isVoteContract) {
|
||||
redirect(`/${params.chain_id}/${params.contractAddress}`);
|
||||
}
|
||||
const [params, account] = await Promise.all([props.params, getRawAccount()]);
|
||||
|
||||
return (
|
||||
<ContractProposalsPage contract={clientContract} isLoggedIn={!!account} />
|
||||
<SharedContractProposalsPage
|
||||
contractAddress={params.contractAddress}
|
||||
chainIdOrSlug={params.chain_id}
|
||||
projectMeta={undefined}
|
||||
isLoggedIn={!!account}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { redirectToContractLandingPage } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/utils";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
|
||||
import { ContractProposalsPage } from "./ContractProposalsPage";
|
||||
import { ContractProposalsPageClient } from "./ContractProposalsPage.client";
|
||||
|
||||
export async function SharedContractProposalsPage(props: {
|
||||
contractAddress: string;
|
||||
chainIdOrSlug: string;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
isLoggedIn: boolean;
|
||||
}) {
|
||||
const info = await getContractPageParamsInfo({
|
||||
contractAddress: props.contractAddress,
|
||||
chainIdOrSlug: props.chainIdOrSlug,
|
||||
teamId: props.projectMeta?.teamId,
|
||||
});
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const { clientContract, serverContract, isLocalhostChain } = info;
|
||||
if (isLocalhostChain) {
|
||||
return (
|
||||
<ContractProposalsPageClient
|
||||
contract={clientContract}
|
||||
isLoggedIn={props.isLoggedIn}
|
||||
projectMeta={props.projectMeta}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const { isVoteContract } = await getContractPageMetadata(serverContract);
|
||||
|
||||
if (!isVoteContract) {
|
||||
redirectToContractLandingPage({
|
||||
projectMeta: props.projectMeta,
|
||||
chainIdOrSlug: props.chainIdOrSlug,
|
||||
contractAddress: props.contractAddress,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<ContractProposalsPage
|
||||
contract={clientContract}
|
||||
isLoggedIn={props.isLoggedIn}
|
||||
/>
|
||||
);
|
||||
}
|
||||
+8
-45
@@ -1,55 +1,18 @@
|
||||
import { DEFAULT_FEE_RECIPIENT } from "constants/addresses";
|
||||
import { notFound } from "next/navigation";
|
||||
import { getPlatformFeeInfo } from "thirdweb/extensions/common";
|
||||
import { getRawAccount } from "../../../../../account/settings/getAccount";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
|
||||
import { ContractSettingsPage } from "./ContractSettingsPage";
|
||||
import { ContractSettingsPageClient } from "./ContractSettingsPage.client";
|
||||
import type { PublicContractPageParams } from "../types";
|
||||
import { SharedContractSettingsPage } from "./shared-settings-page";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: Promise<{
|
||||
contractAddress: string;
|
||||
chain_id: string;
|
||||
}>;
|
||||
params: Promise<PublicContractPageParams>;
|
||||
}) {
|
||||
const params = await props.params;
|
||||
const info = await getContractPageParamsInfo(params);
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const { clientContract, serverContract, isLocalhostChain } = info;
|
||||
|
||||
if (isLocalhostChain) {
|
||||
const account = await getRawAccount();
|
||||
return (
|
||||
<ContractSettingsPageClient
|
||||
contract={clientContract}
|
||||
isLoggedIn={!!account}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const [account, metadata] = await Promise.all([
|
||||
getRawAccount(),
|
||||
getContractPageMetadata(serverContract),
|
||||
]);
|
||||
|
||||
let hasDefaultFeeConfig = true;
|
||||
try {
|
||||
const feeInfo = await getPlatformFeeInfo({ contract: serverContract });
|
||||
hasDefaultFeeConfig =
|
||||
feeInfo[0].toLowerCase() === DEFAULT_FEE_RECIPIENT.toLowerCase();
|
||||
} catch {}
|
||||
const [params, account] = await Promise.all([props.params, getRawAccount()]);
|
||||
|
||||
return (
|
||||
<ContractSettingsPage
|
||||
contract={clientContract}
|
||||
functionSelectors={metadata.functionSelectors}
|
||||
<SharedContractSettingsPage
|
||||
contractAddress={params.contractAddress}
|
||||
chainIdOrSlug={params.chain_id}
|
||||
projectMeta={undefined}
|
||||
isLoggedIn={!!account}
|
||||
hasDefaultFeeConfig={hasDefaultFeeConfig}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
import { DEFAULT_FEE_RECIPIENT } from "constants/addresses";
|
||||
import { notFound } from "next/navigation";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import { getPlatformFeeInfo } from "thirdweb/extensions/common";
|
||||
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
|
||||
import { ContractSettingsPage } from "./ContractSettingsPage";
|
||||
import { ContractSettingsPageClient } from "./ContractSettingsPage.client";
|
||||
|
||||
export async function SharedContractSettingsPage(props: {
|
||||
contractAddress: string;
|
||||
chainIdOrSlug: string;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
isLoggedIn: boolean;
|
||||
}) {
|
||||
const info = await getContractPageParamsInfo({
|
||||
contractAddress: props.contractAddress,
|
||||
chainIdOrSlug: props.chainIdOrSlug,
|
||||
teamId: props.projectMeta?.teamId,
|
||||
});
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const { clientContract, serverContract, isLocalhostChain } = info;
|
||||
|
||||
if (isLocalhostChain) {
|
||||
return (
|
||||
<ContractSettingsPageClient
|
||||
contract={clientContract}
|
||||
isLoggedIn={props.isLoggedIn}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const [metadata, hasDefaultFeeConfig] = await Promise.all([
|
||||
getContractPageMetadata(serverContract),
|
||||
checkDefaultFeeConfig(serverContract),
|
||||
]);
|
||||
|
||||
return (
|
||||
<ContractSettingsPage
|
||||
contract={clientContract}
|
||||
functionSelectors={metadata.functionSelectors}
|
||||
isLoggedIn={props.isLoggedIn}
|
||||
hasDefaultFeeConfig={hasDefaultFeeConfig}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
async function checkDefaultFeeConfig(contract: ThirdwebContract) {
|
||||
let hasDefaultFeeConfig = true;
|
||||
try {
|
||||
const feeInfo = await getPlatformFeeInfo({ contract });
|
||||
hasDefaultFeeConfig =
|
||||
feeInfo[0].toLowerCase() === DEFAULT_FEE_RECIPIENT.toLowerCase();
|
||||
} catch {}
|
||||
|
||||
return hasDefaultFeeConfig;
|
||||
}
|
||||
+218
@@ -0,0 +1,218 @@
|
||||
import { getProjects } from "@/api/projects";
|
||||
import { getTeams } from "@/api/team";
|
||||
import type { MinimalTeamsAndProjects } from "components/contract-components/contract-deploy-form/add-to-project-card";
|
||||
import { resolveFunctionSelectors } from "lib/selectors";
|
||||
import type { Metadata } from "next";
|
||||
import { notFound } from "next/navigation";
|
||||
import { getContractMetadata } from "thirdweb/extensions/common";
|
||||
import { isAddress, isContractDeployed } from "thirdweb/utils";
|
||||
import { shortenIfAddress } from "utils/usedapp-external";
|
||||
import { NebulaChatButton } from "../../../../../nebula-app/(app)/components/FloatingChat/FloatingChat";
|
||||
import { examplePrompts } from "../../../../../nebula-app/(app)/data/examplePrompts";
|
||||
import { getAuthTokenWalletAddress } from "../../../../api/lib/getAuthToken";
|
||||
import type { ProjectMeta } from "../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { ConfigureCustomChain } from "./_layout/ConfigureCustomChain";
|
||||
import { getContractMetadataHeaderData } from "./_layout/contract-metadata";
|
||||
import { ContractPageLayout } from "./_layout/contract-page-layout";
|
||||
import { ContractPageLayoutClient } from "./_layout/contract-page-layout.client";
|
||||
import { supportedERCs } from "./_utils/detectedFeatures/supportedERCs";
|
||||
import { getContractPageParamsInfo } from "./_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "./_utils/getContractPageMetadata";
|
||||
import { getContractPageSidebarLinks } from "./_utils/getContractPageSidebarLinks";
|
||||
|
||||
export async function SharedContractLayout(props: {
|
||||
contractAddress: string;
|
||||
chainIdOrSlug: string;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
if (!isAddress(props.contractAddress)) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
const [info, accountAddress, teamsAndProjects] = await Promise.all([
|
||||
getContractPageParamsInfo({
|
||||
contractAddress: props.contractAddress,
|
||||
chainIdOrSlug: props.chainIdOrSlug,
|
||||
teamId: props.projectMeta?.teamId,
|
||||
}),
|
||||
getAuthTokenWalletAddress(),
|
||||
getTeamsAndProjectsIfLoggedIn(),
|
||||
]);
|
||||
|
||||
if (!info) {
|
||||
return <ConfigureCustomChain chainSlug={props.chainIdOrSlug} />;
|
||||
}
|
||||
|
||||
const { clientContract, serverContract, chainMetadata, isLocalhostChain } =
|
||||
info;
|
||||
|
||||
if (chainMetadata.status === "deprecated") {
|
||||
notFound();
|
||||
}
|
||||
|
||||
if (isLocalhostChain) {
|
||||
return (
|
||||
<ContractPageLayoutClient
|
||||
chainMetadata={chainMetadata}
|
||||
contract={clientContract}
|
||||
teamsAndProjects={teamsAndProjects}
|
||||
projectMeta={props.projectMeta}
|
||||
>
|
||||
{props.children}
|
||||
</ContractPageLayoutClient>
|
||||
);
|
||||
}
|
||||
|
||||
const [
|
||||
isValidContract,
|
||||
contractPageMetadata,
|
||||
{ contractMetadata, externalLinks },
|
||||
] = await Promise.all([
|
||||
isContractDeployed(serverContract).catch(() => false),
|
||||
getContractPageMetadata(serverContract),
|
||||
getContractMetadataHeaderData(serverContract),
|
||||
]);
|
||||
|
||||
// check if the contract exists
|
||||
if (!isValidContract) {
|
||||
// TODO - replace 404 with a better page to upsell deploy or other thirdweb products
|
||||
notFound();
|
||||
}
|
||||
|
||||
const sidebarLinks = getContractPageSidebarLinks({
|
||||
chainSlug: chainMetadata.slug,
|
||||
contractAddress: serverContract.address,
|
||||
metadata: contractPageMetadata,
|
||||
projectMeta: props.projectMeta,
|
||||
});
|
||||
|
||||
const contractAddress = serverContract.address;
|
||||
const chainName = chainMetadata.name;
|
||||
const chainId = chainMetadata.chainId;
|
||||
|
||||
const contractPromptPrefix = `A user is viewing the contract address ${contractAddress} on ${chainName} (Chain ID: ${chainId}). Provide a concise summary of this contract's functionalities, such as token minting, staking, or governance mechanisms. Focus on what the contract enables users to do, avoiding transaction execution details unless requested.
|
||||
Users may be interested in how to interact with the contract. Outline common interaction patterns, such as claiming rewards, participating in governance, or transferring assets. Emphasize the contract's capabilities without guiding through transaction processes unless asked.
|
||||
Provide insights into how the contract is being used. Share information on user engagement, transaction volumes, or integration with other dApps, focusing on the contract's role within the broader ecosystem.
|
||||
Users may be considering integrating the contract into their applications. Discuss how this contract's functionalities can be leveraged within different types of dApps, highlighting potential use cases and benefits.
|
||||
|
||||
The following is the user's message:`;
|
||||
|
||||
return (
|
||||
<ContractPageLayout
|
||||
chainMetadata={chainMetadata}
|
||||
contract={clientContract}
|
||||
sidebarLinks={sidebarLinks}
|
||||
dashboardContractMetadata={contractMetadata}
|
||||
externalLinks={externalLinks}
|
||||
teamsAndProjects={teamsAndProjects}
|
||||
projectMeta={props.projectMeta}
|
||||
>
|
||||
<NebulaChatButton
|
||||
isLoggedIn={!!accountAddress}
|
||||
networks={info.chainMetadata.testnet ? "testnet" : "mainnet"}
|
||||
isFloating={true}
|
||||
pageType="contract"
|
||||
label="Ask AI about this contract"
|
||||
client={clientContract.client}
|
||||
nebulaParams={{
|
||||
messagePrefix: contractPromptPrefix,
|
||||
chainIds: [chainId],
|
||||
wallet: accountAddress ?? undefined,
|
||||
}}
|
||||
examplePrompts={examplePrompts}
|
||||
/>
|
||||
{props.children}
|
||||
</ContractPageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
async function getTeamsAndProjectsIfLoggedIn() {
|
||||
try {
|
||||
const teams = await getTeams();
|
||||
|
||||
if (!teams) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const teamsAndProjects: MinimalTeamsAndProjects = await Promise.all(
|
||||
teams.map(async (team) => ({
|
||||
team: {
|
||||
id: team.id,
|
||||
name: team.name,
|
||||
slug: team.slug,
|
||||
image: team.image,
|
||||
},
|
||||
projects: (await getProjects(team.slug)).map((x) => ({
|
||||
id: x.id,
|
||||
name: x.name,
|
||||
image: x.image,
|
||||
})),
|
||||
})),
|
||||
);
|
||||
|
||||
return teamsAndProjects;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export async function generateContractLayoutMetadata(params: {
|
||||
contractAddress: string;
|
||||
chainIdOrSlug: string;
|
||||
}): Promise<Metadata> {
|
||||
try {
|
||||
const info = await getContractPageParamsInfo({
|
||||
contractAddress: params.contractAddress,
|
||||
chainIdOrSlug: params.chainIdOrSlug,
|
||||
teamId: undefined,
|
||||
});
|
||||
|
||||
if (!info) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
const [functionSelectors, contractMetadata] = await Promise.all([
|
||||
resolveFunctionSelectors(info.serverContract),
|
||||
getContractMetadata({
|
||||
contract: info.serverContract,
|
||||
}),
|
||||
]);
|
||||
|
||||
if (contractMetadata.name === null) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
const { isERC1155, isERC20, isERC721 } = supportedERCs(functionSelectors);
|
||||
|
||||
const contractDisplayName = `${contractMetadata.name}${
|
||||
contractMetadata.symbol ? ` (${contractMetadata.symbol})` : ""
|
||||
}`;
|
||||
|
||||
const cleanedChainName = info?.chainMetadata?.name
|
||||
.replace("Mainnet", "")
|
||||
.replace("Testnet", "")
|
||||
.trim();
|
||||
|
||||
const title = `${contractDisplayName} | ${cleanedChainName} Smart Contract`;
|
||||
let description = "";
|
||||
|
||||
if (isERC721 || isERC1155) {
|
||||
description = `View tokens, source code, transactions, balances, and analytics for the ${contractDisplayName} smart contract on ${cleanedChainName}.`;
|
||||
} else if (isERC20) {
|
||||
description = `View ERC20 tokens, transactions, balances, source code, and analytics for the ${contractDisplayName} smart contract on ${cleanedChainName}`;
|
||||
} else {
|
||||
description = `View tokens, transactions, balances, source code, and analytics for the ${contractDisplayName} smart contract on ${cleanedChainName}`;
|
||||
}
|
||||
|
||||
return {
|
||||
title: title,
|
||||
description: description,
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
title: `${shortenIfAddress(params.contractAddress)} | ${params.chainIdOrSlug}`,
|
||||
description: `View tokens, transactions, balances, source code, and analytics for the smart contract on Chain ID ${params.chainIdOrSlug}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { ErrorBoundary } from "react-error-boundary";
|
||||
import type { ProjectMeta } from "../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { getContractPageParamsInfo } from "./_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "./_utils/getContractPageMetadata";
|
||||
import { ContractOverviewPage } from "./overview/ContractOverviewPage";
|
||||
import { PublishedBy } from "./overview/components/published-by.server";
|
||||
import { ContractOverviewPageClient } from "./overview/contract-overview-page.client";
|
||||
|
||||
export async function SharedContractOverviewPage(props: {
|
||||
contractAddress: string;
|
||||
chainIdOrSlug: string;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
}) {
|
||||
const info = await getContractPageParamsInfo({
|
||||
contractAddress: props.contractAddress,
|
||||
chainIdOrSlug: props.chainIdOrSlug,
|
||||
teamId: props.projectMeta?.teamId,
|
||||
});
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const { clientContract, serverContract, chainMetadata, isLocalhostChain } =
|
||||
info;
|
||||
if (isLocalhostChain) {
|
||||
return (
|
||||
<ContractOverviewPageClient
|
||||
projectMeta={props.projectMeta}
|
||||
chainMetadata={chainMetadata}
|
||||
contract={clientContract}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const contractPageMetadata = await getContractPageMetadata(serverContract);
|
||||
|
||||
return (
|
||||
<ContractOverviewPage
|
||||
contract={clientContract}
|
||||
projectMeta={props.projectMeta}
|
||||
hasDirectListings={contractPageMetadata.isDirectListingSupported}
|
||||
hasEnglishAuctions={contractPageMetadata.isEnglishAuctionSupported}
|
||||
isErc1155={contractPageMetadata.supportedERCs.isERC1155}
|
||||
isErc20={contractPageMetadata.supportedERCs.isERC20}
|
||||
isErc721={contractPageMetadata.supportedERCs.isERC721}
|
||||
isPermissionsEnumerable={
|
||||
contractPageMetadata.isPermissionsEnumerableSupported
|
||||
}
|
||||
chainSlug={chainMetadata.slug}
|
||||
isAnalyticsSupported={contractPageMetadata.isInsightSupported}
|
||||
functionSelectors={contractPageMetadata.functionSelectors}
|
||||
publishedBy={
|
||||
<ErrorBoundary fallback={null}>
|
||||
<PublishedBy contract={serverContract} />
|
||||
</ErrorBoundary>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
+10
-14
@@ -1,19 +1,15 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { ContractSourcesPage } from "./ContractSourcesPage";
|
||||
import type { PublicContractPageParams } from "../types";
|
||||
import { SharedContractSourcesPage } from "./shared-sources-page";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: Promise<{
|
||||
contractAddress: string;
|
||||
chain_id: string;
|
||||
}>;
|
||||
params: Promise<PublicContractPageParams>;
|
||||
}) {
|
||||
const params = await props.params;
|
||||
const info = await getContractPageParamsInfo(params);
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return <ContractSourcesPage contract={info.clientContract} />;
|
||||
return (
|
||||
<SharedContractSourcesPage
|
||||
contractAddress={params.contractAddress}
|
||||
chainIdOrSlug={params.chain_id}
|
||||
projectMeta={undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { ContractSourcesPage } from "./ContractSourcesPage";
|
||||
|
||||
export async function SharedContractSourcesPage(props: {
|
||||
contractAddress: string;
|
||||
chainIdOrSlug: string;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
}) {
|
||||
const info = await getContractPageParamsInfo({
|
||||
contractAddress: props.contractAddress,
|
||||
chainIdOrSlug: props.chainIdOrSlug,
|
||||
teamId: props.projectMeta?.teamId,
|
||||
});
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return <ContractSourcesPage contract={info.clientContract} />;
|
||||
}
|
||||
+8
-1
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { ErrorPage, LoadingPage } from "../_components/page-skeletons";
|
||||
import { RedirectToContractOverview } from "../_components/redirect-contract-overview.client";
|
||||
import { useContractPageMetadata } from "../_hooks/useContractPageMetadata";
|
||||
@@ -9,6 +10,7 @@ import { ContractSplitPage } from "./ContractSplitPage";
|
||||
export function ContractSplitPageClient(props: {
|
||||
contract: ThirdwebContract;
|
||||
isLoggedIn: boolean;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
}) {
|
||||
const metadataQuery = useContractPageMetadata(props.contract);
|
||||
|
||||
@@ -21,7 +23,12 @@ export function ContractSplitPageClient(props: {
|
||||
}
|
||||
|
||||
if (!metadataQuery.data.isSplitSupported) {
|
||||
return <RedirectToContractOverview contract={props.contract} />;
|
||||
return (
|
||||
<RedirectToContractOverview
|
||||
contract={props.contract}
|
||||
projectMeta={props.projectMeta}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
+10
-34
@@ -1,42 +1,18 @@
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { getRawAccount } from "../../../../../account/settings/getAccount";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
|
||||
import { ContractSplitPage } from "./ContractSplitPage";
|
||||
import { ContractSplitPageClient } from "./ContractSplitPage.client";
|
||||
import type { PublicContractPageParams } from "../types";
|
||||
import { SharedContractSplitPage } from "./shared-split-page";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: Promise<{
|
||||
contractAddress: string;
|
||||
chain_id: string;
|
||||
}>;
|
||||
params: Promise<PublicContractPageParams>;
|
||||
}) {
|
||||
const params = await props.params;
|
||||
const info = await getContractPageParamsInfo(params);
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
const { clientContract, serverContract, isLocalhostChain } = info;
|
||||
|
||||
const twAccount = await getRawAccount();
|
||||
|
||||
if (isLocalhostChain) {
|
||||
return (
|
||||
<ContractSplitPageClient
|
||||
contract={clientContract}
|
||||
isLoggedIn={!!twAccount}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const { isSplitSupported } = await getContractPageMetadata(serverContract);
|
||||
|
||||
if (!isSplitSupported) {
|
||||
redirect(`/${params.chain_id}/${params.contractAddress}`);
|
||||
}
|
||||
const [params, account] = await Promise.all([props.params, getRawAccount()]);
|
||||
|
||||
return (
|
||||
<ContractSplitPage contract={clientContract} isLoggedIn={!!twAccount} />
|
||||
<SharedContractSplitPage
|
||||
contractAddress={params.contractAddress}
|
||||
chainIdOrSlug={params.chain_id}
|
||||
projectMeta={undefined}
|
||||
isLoggedIn={!!account}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { redirectToContractLandingPage } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/utils";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
|
||||
import { ContractSplitPage } from "./ContractSplitPage";
|
||||
import { ContractSplitPageClient } from "./ContractSplitPage.client";
|
||||
|
||||
export async function SharedContractSplitPage(props: {
|
||||
contractAddress: string;
|
||||
chainIdOrSlug: string;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
isLoggedIn: boolean;
|
||||
}) {
|
||||
const info = await getContractPageParamsInfo({
|
||||
contractAddress: props.contractAddress,
|
||||
chainIdOrSlug: props.chainIdOrSlug,
|
||||
teamId: props.projectMeta?.teamId,
|
||||
});
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
const { clientContract, serverContract, isLocalhostChain } = info;
|
||||
|
||||
if (isLocalhostChain) {
|
||||
return (
|
||||
<ContractSplitPageClient
|
||||
projectMeta={props.projectMeta}
|
||||
contract={clientContract}
|
||||
isLoggedIn={props.isLoggedIn}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const { isSplitSupported } = await getContractPageMetadata(serverContract);
|
||||
|
||||
if (!isSplitSupported) {
|
||||
redirectToContractLandingPage({
|
||||
chainIdOrSlug: props.chainIdOrSlug,
|
||||
contractAddress: props.contractAddress,
|
||||
projectMeta: props.projectMeta,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<ContractSplitPage
|
||||
contract={clientContract}
|
||||
isLoggedIn={props.isLoggedIn}
|
||||
/>
|
||||
);
|
||||
}
|
||||
+8
-39
@@ -1,48 +1,17 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import {
|
||||
isClaimToSupported,
|
||||
isMintToSupported,
|
||||
} from "thirdweb/extensions/erc20";
|
||||
import { getRawAccount } from "../../../../../account/settings/getAccount";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
|
||||
import { ContractTokensPage } from "./ContractTokensPage";
|
||||
import { ContractTokensPageClient } from "./ContractTokensPage.client";
|
||||
import type { PublicContractPageParams } from "../types";
|
||||
import { SharedContractTokensPage } from "./shared-page";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: Promise<{
|
||||
contractAddress: string;
|
||||
chain_id: string;
|
||||
}>;
|
||||
params: Promise<PublicContractPageParams>;
|
||||
}) {
|
||||
const params = await props.params;
|
||||
const info = await getContractPageParamsInfo(params);
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const account = await getRawAccount();
|
||||
|
||||
if (info.isLocalhostChain) {
|
||||
return (
|
||||
<ContractTokensPageClient
|
||||
contract={info.clientContract}
|
||||
isLoggedIn={!!account}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const { supportedERCs, functionSelectors } = await getContractPageMetadata(
|
||||
info.serverContract,
|
||||
);
|
||||
const [params, account] = await Promise.all([props.params, getRawAccount()]);
|
||||
|
||||
return (
|
||||
<ContractTokensPage
|
||||
contract={info.clientContract}
|
||||
isERC20={supportedERCs.isERC20}
|
||||
isMintToSupported={isMintToSupported(functionSelectors)}
|
||||
isClaimToSupported={isClaimToSupported(functionSelectors)}
|
||||
<SharedContractTokensPage
|
||||
contractAddress={params.contractAddress}
|
||||
chainIdOrSlug={params.chain_id}
|
||||
projectMeta={undefined}
|
||||
isLoggedIn={!!account}
|
||||
/>
|
||||
);
|
||||
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import {
|
||||
isClaimToSupported,
|
||||
isMintToSupported,
|
||||
} from "thirdweb/extensions/erc20";
|
||||
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
|
||||
import { ContractTokensPage } from "./ContractTokensPage";
|
||||
import { ContractTokensPageClient } from "./ContractTokensPage.client";
|
||||
|
||||
export async function SharedContractTokensPage(props: {
|
||||
contractAddress: string;
|
||||
chainIdOrSlug: string;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
isLoggedIn: boolean;
|
||||
}) {
|
||||
const info = await getContractPageParamsInfo({
|
||||
contractAddress: props.contractAddress,
|
||||
chainIdOrSlug: props.chainIdOrSlug,
|
||||
teamId: props.projectMeta?.teamId,
|
||||
});
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
if (info.isLocalhostChain) {
|
||||
return (
|
||||
<ContractTokensPageClient
|
||||
contract={info.clientContract}
|
||||
isLoggedIn={props.isLoggedIn}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const { supportedERCs, functionSelectors } = await getContractPageMetadata(
|
||||
info.serverContract,
|
||||
);
|
||||
|
||||
return (
|
||||
<ContractTokensPage
|
||||
contract={info.clientContract}
|
||||
isERC20={supportedERCs.isERC20}
|
||||
isMintToSupported={isMintToSupported(functionSelectors)}
|
||||
isClaimToSupported={isClaimToSupported(functionSelectors)}
|
||||
isLoggedIn={props.isLoggedIn}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export type PublicContractPageParams = {
|
||||
contractAddress: string;
|
||||
chain_id: string;
|
||||
};
|
||||
@@ -12,6 +12,8 @@ export function DeployedContractsPage(props: {
|
||||
projectId: string;
|
||||
authToken: string;
|
||||
client: ThirdwebClient;
|
||||
teamSlug: string;
|
||||
projectSlug: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex grow flex-col">
|
||||
@@ -39,6 +41,8 @@ async function DeployedContractsPageAsync(props: {
|
||||
projectId: string;
|
||||
authToken: string;
|
||||
client: ThirdwebClient;
|
||||
teamSlug: string;
|
||||
projectSlug: string;
|
||||
}) {
|
||||
const deployedContracts = await getSortedDeployedContracts({
|
||||
teamId: props.teamId,
|
||||
@@ -56,6 +60,8 @@ async function DeployedContractsPageAsync(props: {
|
||||
teamId={props.teamId}
|
||||
projectId={props.projectId}
|
||||
client={props.client}
|
||||
teamSlug={props.teamSlug}
|
||||
projectSlug={props.projectSlug}
|
||||
/>
|
||||
</ClientOnly>
|
||||
);
|
||||
|
||||
@@ -29,8 +29,18 @@ export async function getAuthTokenWalletAddress() {
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function getUserThirdwebClient() {
|
||||
export async function getUserThirdwebClient(params: {
|
||||
teamId: string | undefined;
|
||||
}) {
|
||||
const authToken = await getAuthToken();
|
||||
|
||||
if (params.teamId) {
|
||||
return getClientThirdwebClient({
|
||||
jwt: authToken,
|
||||
teamId: params.teamId,
|
||||
});
|
||||
}
|
||||
|
||||
const cookiesManager = await cookies();
|
||||
const lastUsedTeamId = cookiesManager.get(LAST_USED_TEAM_ID)?.value;
|
||||
|
||||
|
||||
@@ -65,6 +65,8 @@ export default async function Page(props: {
|
||||
projectId={project.id}
|
||||
authToken={authToken}
|
||||
client={client}
|
||||
teamSlug={params.team_slug}
|
||||
projectSlug={params.project_slug}
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
@@ -92,6 +94,8 @@ async function AssetsPageAsync(props: {
|
||||
projectId: string;
|
||||
authToken: string;
|
||||
client: ThirdwebClient;
|
||||
teamSlug: string;
|
||||
projectSlug: string;
|
||||
}) {
|
||||
const deployedContracts = await getSortedDeployedContracts({
|
||||
teamId: props.teamId,
|
||||
@@ -109,6 +113,8 @@ async function AssetsPageAsync(props: {
|
||||
teamId={props.teamId}
|
||||
projectId={props.projectId}
|
||||
client={props.client}
|
||||
teamSlug={props.teamSlug}
|
||||
projectSlug={props.projectSlug}
|
||||
/>
|
||||
</ClientOnly>
|
||||
);
|
||||
|
||||
+11
@@ -94,6 +94,17 @@ export function ProjectSidebarLayout(props: {
|
||||
icon: NebulaIcon,
|
||||
tracking: tracking("nebula"),
|
||||
},
|
||||
// Commented until we solve the scrolling issue
|
||||
// {
|
||||
// href: `${layoutPath}/webhooks`,
|
||||
// label: (
|
||||
// <span className="flex items-center gap-2">
|
||||
// Webhooks <Badge>New</Badge>
|
||||
// </span>
|
||||
// ),
|
||||
// icon: BellIcon,
|
||||
// tracking: tracking("webhooks"),
|
||||
// },
|
||||
]}
|
||||
footerSidebarLinks={[
|
||||
{
|
||||
|
||||
+10
@@ -50,6 +50,8 @@ export default async function Page(props: {
|
||||
<YourFactoriesSection
|
||||
teamId={team.id}
|
||||
projectId={project.id}
|
||||
teamSlug={team.slug}
|
||||
projectSlug={project.slug}
|
||||
authToken={authToken}
|
||||
/>
|
||||
</div>
|
||||
@@ -60,6 +62,8 @@ function YourFactoriesSection(props: {
|
||||
teamId: string;
|
||||
projectId: string;
|
||||
authToken: string;
|
||||
teamSlug: string;
|
||||
projectSlug: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
@@ -98,6 +102,8 @@ function YourFactoriesSection(props: {
|
||||
teamId={props.teamId}
|
||||
projectId={props.projectId}
|
||||
authToken={props.authToken}
|
||||
teamSlug={props.teamSlug}
|
||||
projectSlug={props.projectSlug}
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
@@ -108,6 +114,8 @@ async function AsyncYourFactories(props: {
|
||||
teamId: string;
|
||||
projectId: string;
|
||||
authToken: string;
|
||||
teamSlug: string;
|
||||
projectSlug: string;
|
||||
}) {
|
||||
const deployedContracts = await getSortedDeployedContracts({
|
||||
teamId: props.teamId,
|
||||
@@ -140,6 +148,8 @@ async function AsyncYourFactories(props: {
|
||||
<FactoryContracts
|
||||
contracts={factories}
|
||||
isPending={false}
|
||||
teamSlug={props.teamSlug}
|
||||
projectSlug={props.projectSlug}
|
||||
isFetched={true}
|
||||
/>
|
||||
</ClientOnly>
|
||||
|
||||
+14
-6
@@ -2,6 +2,8 @@ import { getProject } from "@/api/projects";
|
||||
import { getTeamBySlug } from "@/api/team";
|
||||
import { getFees } from "@/api/universal-bridge/developer";
|
||||
import { PayConfig } from "components/pay/PayConfig";
|
||||
import { RouteDiscovery } from "components/pay/RouteDiscovery";
|
||||
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export default async function Page(props: {
|
||||
@@ -47,11 +49,17 @@ export default async function Page(props: {
|
||||
}
|
||||
|
||||
return (
|
||||
<PayConfig
|
||||
project={project}
|
||||
teamId={team.id}
|
||||
teamSlug={team_slug}
|
||||
fees={fees}
|
||||
/>
|
||||
<div className="flex flex-col p-5">
|
||||
<PayConfig
|
||||
project={project}
|
||||
teamId={team.id}
|
||||
teamSlug={team_slug}
|
||||
fees={fees}
|
||||
/>
|
||||
|
||||
<div className="flex pt-5">
|
||||
<RouteDiscovery project={project} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+6
-4
@@ -111,8 +111,8 @@ export function PayWebhooksPage(props: PayWebhooksPageProps) {
|
||||
<TableHead>Label</TableHead>
|
||||
<TableHead>Url</TableHead>
|
||||
<TableHead>Created</TableHead>
|
||||
<TableHead>Version</TableHead>
|
||||
<TableHead>Delete</TableHead>
|
||||
<TableHead className="text-center">Version</TableHead>
|
||||
<TableHead className="text-center">Delete</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
@@ -123,8 +123,10 @@ export function PayWebhooksPage(props: PayWebhooksPageProps) {
|
||||
<TableCell>
|
||||
{formatDistanceToNow(webhook.createdAt, { addSuffix: true })}
|
||||
</TableCell>
|
||||
<TableCell>{webhook.version || "v1"}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<TableCell className="text-center">
|
||||
{webhook.version || "1"}
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<DeleteWebhookButton
|
||||
clientId={props.clientId}
|
||||
teamId={props.teamId}
|
||||
|
||||
+2
@@ -42,6 +42,8 @@ export default async function Page(props: {
|
||||
projectId={project.id}
|
||||
authToken={authToken}
|
||||
client={client}
|
||||
teamSlug={params.team_slug}
|
||||
projectSlug={params.project_slug}
|
||||
/>
|
||||
<div className="h-20" />
|
||||
<ContractsFooter />
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user