[Add] Support for modular contracts in explore and deploy pages (#4288)

<!-- start pr-codex -->

## PR-Codex overview
This PR updates contract components and SDK with default modules support.

### Detailed summary
- Added `defaultModules` support in SDK and contract components
- Updated contract card to display modules
- Added transfer validator field in royalty fieldset
- Improved contract publishing information display

> The following files were skipped due to too many changes: `apps/dashboard/src/components/contract-components/shared/published-by.tsx`, `apps/dashboard/src/components/contract-components/contract-deploy-form/custom-contract.tsx`, `apps/dashboard/src/components/explore/contract-card/index.tsx`, `apps/dashboard/src/components/pages/publish.tsx`, `apps/dashboard/src/data/explore.ts`

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
This commit is contained in:
jnsdls
2024-09-10 00:12:40 +00:00
parent 1ceaa72bef
commit bb9ce26d6b
15 changed files with 547 additions and 133 deletions
@@ -24,14 +24,19 @@ import { useTxNotifications } from "hooks/useTxNotifications";
import { replaceTemplateValues } from "lib/deployment/template-values";
import { ExternalLinkIcon, InfoIcon } from "lucide-react";
import Link from "next/link";
import { useSearchParams } from "next/navigation";
import { useRouter } from "next/router";
import { useMemo } from "react";
import { FormProvider, type UseFormReturn, useForm } from "react-hook-form";
import { FiHelpCircle } from "react-icons/fi";
import { ZERO_ADDRESS } from "thirdweb";
import { computePublishedContractAddress } from "thirdweb/deploys";
import { useActiveAccount } from "thirdweb/react";
import { encodeAbiParameters } from "thirdweb/utils";
import invariant from "tiny-invariant";
import { FormHelperText, FormLabel, Heading, Text } from "tw-components";
import { thirdwebClient } from "../../../@/constants/client";
import { useV5DashboardChain } from "../../../lib/v5-adapter";
import {
useContractEnabledExtensions,
useContractFullPublishMetadata,
@@ -77,6 +82,11 @@ export type CustomContractDeploymentFormData = {
saltForCreate2: string;
signerAsSalt: boolean;
deployParams: Record<string, string>;
defaultModules: {
moduleName: string;
moduleVersion: string;
publisherAddress: string;
}[];
modularContractDefaultModulesInstallParams: Record<string, string>[];
contractMetadata?: {
name: string;
@@ -120,6 +130,8 @@ const CustomContractForm: React.FC<CustomContractFormProps> = ({
const connectedWallet = ensQuery.data?.address || walletAddress;
const trackEvent = useTrack();
const activeAccount = useActiveAccount();
const searchParams = useSearchParams();
const chain = useV5DashboardChain(selectedChain);
const compilerMetadata = useContractPublishMetadataFromURI(ipfsHash);
const fullPublishMetadata = useContractFullPublishMetadata(ipfsHash);
@@ -160,7 +172,14 @@ const CustomContractForm: React.FC<CustomContractFormProps> = ({
fullPublishMetadata.data?.deployType === "customFactory";
const isModular = fullPublishMetadata.data?.routerType === "modular";
const defaultModules = fullPublishMetadata.data?.defaultModules;
const defaultModules = (searchParams?.getAll("module") || [])
.map((m) => JSON.parse(atob(m)))
.map((m) => ({
publisherAddress: m.publisher,
moduleName: m.moduleId,
moduleVersion: m.version || "latest",
}));
const modularContractDefaultModulesInstallParams =
useModularContractsDefaultModulesInstallParams({
@@ -218,6 +237,7 @@ const CustomContractForm: React.FC<CustomContractFormProps> = ({
signerAsSalt: true,
deployParams: parsedDeployParams,
recipients: [{ address: connectedWallet || "", sharesBps: 10000 }],
defaultModules,
// set default values for modular contract modules with custom components
modularContractDefaultModulesInstallParams:
(activeAccount &&
@@ -229,6 +249,8 @@ const CustomContractForm: React.FC<CustomContractFormProps> = ({
// set connected wallet address as default "royaltyRecipient"
if (showRoyaltyFieldset(paramNames)) {
returnVal.royaltyRecipient = activeAccount.address;
returnVal.royaltyBps = "0";
returnVal.transferValidator = ZERO_ADDRESS;
}
// set connected wallet address as default "primarySaleRecipient"
@@ -247,6 +269,7 @@ const CustomContractForm: React.FC<CustomContractFormProps> = ({
modularContractDefaultModulesInstallParams.data,
isTWPublisher,
activeAccount,
defaultModules,
],
);
@@ -395,7 +418,7 @@ const CustomContractForm: React.FC<CustomContractFormProps> = ({
id="custom-contract-form"
as="form"
onSubmit={form.handleSubmit(async (formData) => {
if (!selectedChain) {
if (!selectedChain || !chain) {
return;
}
const deployData = {
@@ -436,7 +459,20 @@ const CustomContractForm: React.FC<CustomContractFormProps> = ({
},
);
const moduleInstallationAddresses: string[] = await Promise.all(
modularContractDefaultModulesInstallParams.data.map((d) =>
computePublishedContractAddress({
chain,
client: thirdwebClient,
contractId: d.moduleName,
// modules never have consturctor params so this is fine!
constructorParams: [],
}),
),
);
deployParams._moduleInstallData = JSON.stringify(moduleInstallData);
deployParams._modules = JSON.stringify(moduleInstallationAddresses);
}
deploy.mutate(
@@ -177,6 +177,9 @@ function RenderRoyaltyFieldset(props: {
const royaltyBpsPath =
`modularContractDefaultModulesInstallParams.${ext.moduleIndex}.royaltyBps` as const;
const transferValidatorPath =
`modularContractDefaultModulesInstallParams.${ext.moduleIndex}.transferValidator` as const;
return (
<RoyaltyFieldset
royaltyRecipient={{
@@ -198,15 +201,25 @@ function RenderRoyaltyFieldset(props: {
errorMessage: form.getFieldState(royaltyBpsPath, form.formState).error
?.message,
}}
transferValidator={{
isInvalid: !!form.getFieldState(transferValidatorPath, form.formState)
.error,
register: form.register(transferValidatorPath, {
required: "Required",
}),
errorMessage: form.getFieldState(transferValidatorPath, form.formState)
.error?.message,
}}
/>
);
}
export function showRoyaltyFieldset(paramNames: string[]) {
return (
paramNames.length === 2 &&
paramNames.length === 3 &&
paramNames.includes("royaltyRecipient") &&
paramNames.includes("royaltyBps")
paramNames.includes("royaltyBps") &&
paramNames.includes("transferValidator")
);
}
@@ -17,40 +17,65 @@ export function RoyaltyFieldset(props: {
value: string;
setValue: (value: string) => void;
};
transferValidator?: {
isInvalid: boolean;
register: UseFormRegisterReturn;
errorMessage: string | undefined;
};
}) {
const bpsNumValue = Number.parseInt(props.royaltyBps.value);
return (
<Fieldset legend="Royalties">
<div className="flex flex-col gap-4 md:flex-row">
<FormControl isRequired isInvalid={props.royaltyRecipient.isInvalid}>
<FormLabel>Recipient Address</FormLabel>
<SolidityInput
solidityType="address"
variant="filled"
{...props.royaltyRecipient.register}
/>
<FormErrorMessage>
{props.royaltyRecipient.errorMessage}
</FormErrorMessage>
<FormHelperText className="!text-sm text-muted-foreground">
The wallet address that should receive the revenue from royalties
earned from secondary sales of the assets.
</FormHelperText>
</FormControl>
<FormControl
isRequired
maxW={{ base: "100%", md: "150px" }}
isInvalid={props.royaltyBps.isInvalid}
defaultValue="0"
>
<FormLabel>Percentage</FormLabel>
<BasisPointsInput
variant="filled"
value={Number.isNaN(bpsNumValue) ? 0 : bpsNumValue}
onChange={(value) => props.royaltyBps.setValue(value.toString())}
/>
<FormErrorMessage>{props.royaltyBps.errorMessage}</FormErrorMessage>
</FormControl>
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-4 md:flex-row">
<FormControl isRequired isInvalid={props.royaltyRecipient.isInvalid}>
<FormLabel>Recipient Address</FormLabel>
<SolidityInput
solidityType="address"
variant="filled"
{...props.royaltyRecipient.register}
/>
<FormErrorMessage>
{props.royaltyRecipient.errorMessage}
</FormErrorMessage>
<FormHelperText className="!text-sm text-muted-foreground">
The wallet address that should receive the revenue from royalties
earned from secondary sales of the assets.
</FormHelperText>
</FormControl>
<FormControl
isRequired
maxW={{ base: "100%", md: "150px" }}
isInvalid={props.royaltyBps.isInvalid}
defaultValue="0"
>
<FormLabel>Percentage</FormLabel>
<BasisPointsInput
variant="filled"
value={Number.isNaN(bpsNumValue) ? 0 : bpsNumValue}
onChange={(value) => props.royaltyBps.setValue(value.toString())}
/>
<FormErrorMessage>{props.royaltyBps.errorMessage}</FormErrorMessage>
</FormControl>
</div>
{props.transferValidator && (
<FormControl isRequired isInvalid={props.transferValidator.isInvalid}>
<FormLabel>Transfer Validator Address</FormLabel>
<SolidityInput
solidityType="address"
variant="filled"
{...props.transferValidator.register}
/>
<FormErrorMessage>
{props.transferValidator.errorMessage}
</FormErrorMessage>
<FormHelperText className="!text-sm text-muted-foreground">
The contract address to enforce royalties according to the Creator
Token Standard implementation. Passing the zero address disables
this validation.
</FormHelperText>
</FormControl>
)}
</div>
</Fieldset>
);
@@ -840,6 +840,7 @@ export function useCustomContractDeployMutation(options: {
Object.values(data.deployParams),
{
forceDirectDeploy,
defaultModules: data.defaultModules,
},
);
}
@@ -1,4 +1,5 @@
import { useEVMContractInfo } from "@3rdweb-sdk/react";
import { useQuery } from "@tanstack/react-query";
import {
useEns,
usePublishedContractsFromDeploy,
@@ -6,16 +7,21 @@ import {
import { ContractCard } from "components/explore/contract-card";
import { THIRDWEB_DEPLOYER_ADDRESS } from "constants/addresses";
import { useMemo } from "react";
import { useActiveAccount } from "thirdweb/react";
import type { ThirdwebContract } from "thirdweb";
import { polygon } from "thirdweb/chains";
import { getBytecode, getContract } from "thirdweb/contract";
import { getPublishedUriFromCompilerUri } from "thirdweb/extensions/thirdweb";
import { getInstalledModules } from "thirdweb/modules";
import { useActiveAccount, useReadContract } from "thirdweb/react";
import { download } from "thirdweb/storage";
import { extractIPFSUri } from "thirdweb/utils";
interface PublishedByProps {
contractAddress: string;
contract: ThirdwebContract;
}
export const PublishedBy: React.FC<PublishedByProps> = ({
contractAddress,
}) => {
const contractEnsQuery = useEns(contractAddress);
export const PublishedBy: React.FC<PublishedByProps> = ({ contract }) => {
const contractEnsQuery = useEns(contract.address);
const activeNetworkInfo = useEVMContractInfo();
const publishedContractsFromDeploy = usePublishedContractsFromDeploy(
@@ -49,6 +55,78 @@ export const PublishedBy: React.FC<PublishedByProps> = ({
const publisherAddress =
publisherEnsQuery.data?.ensName || publisherEnsQuery.data?.address;
const installedModules = useReadContract(getInstalledModules, {
contract,
queryOptions: {
enabled: publishedContractToShow?.routerType === "modular" && !!contract,
},
});
// this handles all the logic for modules in the published contract card
const installedModulesQuery = useQuery({
queryKey: [
"published-by-modules",
contract,
installedModules.data?.map((m) => m.implementation),
],
queryFn: async () => {
if (!installedModules.data?.length) {
return [];
}
const moduleContracts = installedModules.data.map((module) => {
return getContract({ ...contract, address: module.implementation });
});
const metadataUris = await Promise.allSettled(
moduleContracts.map(async (c) => {
const byteCode = await getBytecode(c);
const ipfsUri = extractIPFSUri(byteCode);
if (!ipfsUri) {
throw new Error("No IPFS URI found in bytecode");
}
let uris = await getPublishedUriFromCompilerUri({
contract: {
chain: polygon,
client: contract.client,
address: "0xf5b896Ddb5146D5dA77efF4efBb3Eae36E300808",
},
compilerMetadataUri: ipfsUri,
}).catch((e) => {
console.error("Error fetching published URI", e);
return [];
});
uris = uris.filter((uri) => uri.length > 0);
if (uris.length === 0) {
throw new Error("No published URI found");
}
const results = await Promise.allSettled(
uris.map(async (uri) => {
const content = await download({
uri,
client: contract.client,
});
return JSON.parse(await content.text());
}),
);
return results
.filter((r) => r.status === "fulfilled")
.map((r) => r.value);
}),
);
const filtered = metadataUris
.filter((m) => m.status === "fulfilled")
.map((m) => m.value);
return filtered.map((m) => m[0]);
},
enabled: !!installedModules.data?.length,
});
if (!publishedContractToShow || !publisherAddress) {
return null;
}
@@ -61,6 +139,11 @@ export const PublishedBy: React.FC<PublishedByProps> = ({
isBeta={(publishedContractToShow.displayName || "")
.toLowerCase()
.includes("beta")}
modules={installedModulesQuery.data?.map((m) => ({
publisher: m.publisher,
moduleId: m.name,
version: m.version,
}))}
/>
);
};
@@ -13,7 +13,6 @@ import { getDashboardChainRpc } from "lib/rpc";
import { getThirdwebSDK, replaceIpfsUrl } from "lib/sdk";
import { RocketIcon, ShieldCheckIcon } from "lucide-react";
import Link from "next/link";
import { useMemo } from "react";
import { polygon } from "thirdweb/chains";
import invariant from "tiny-invariant";
import { ContractPublisher, replaceDeployerAddress } from "../publisher";
@@ -21,43 +20,84 @@ import { ContractPublisher, replaceDeployerAddress } from "../publisher";
interface ContractCardProps {
publisher: string;
contractId: string;
titleOverride?: string;
descriptionOverride?: string;
version?: string;
tracking?: {
source: string;
itemIndex: `${number}`;
};
isBeta: boolean | undefined;
// for modular contracts to show the modules in the card
// publisher and moduleId are required
// version is optional
// if version is not provided, it will default to "latest"
modules?: {
publisher: string;
moduleId: string;
version?: string;
}[];
}
function getContractUrl(
{
version,
publisher,
contractId,
titleOverride,
modules = [],
}: {
publisher: string;
contractId: string;
version?: string;
titleOverride?: string;
modules?: {
publisher: string;
moduleId: string;
version?: string;
}[];
},
isDeploy?: true,
) {
let pathName = "";
if (version !== "latest") {
pathName = `/${publisher}/${contractId}/${version}`;
} else {
pathName = `/${publisher}/${contractId}`;
}
if (isDeploy) {
pathName += "/deploy";
}
const moudleUrl = new URLSearchParams();
for (const m of modules) {
moudleUrl.append("module", btoa(JSON.stringify(m)));
}
if (titleOverride) {
moudleUrl.append("displayName", titleOverride);
}
pathName += `?${moudleUrl.toString()}`;
return replaceDeployerAddress(pathName);
}
export const ContractCard: React.FC<ContractCardProps> = ({
publisher,
contractId,
titleOverride,
descriptionOverride,
version = "latest",
tracking,
modules = [],
isBeta,
}) => {
const publishedContractResult = usePublishedContract(
`${publisher}/${contractId}/${version}`,
);
const isNewContract = useMemo(() => {
const newContracts = ["thirdweb.eth/AccountFactory"];
return newContracts.includes(`${publisher}/${contractId}`);
}, [publisher, contractId]);
const showSkeleton = publishedContractResult.isLoading;
const href = useMemo(() => {
let h: string;
if (version !== "latest") {
h = `/${publisher}/${contractId}/${version}`;
} else {
h = `/${publisher}/${contractId}`;
}
return replaceDeployerAddress(h);
}, [contractId, publisher, version]);
return (
<article
className={cn(
@@ -67,7 +107,13 @@ export const ContractCard: React.FC<ContractCardProps> = ({
>
<TrackedLinkTW
className="absolute inset-0 z-0 cursor-pointer"
href={href}
href={getContractUrl({
publisher,
contractId,
version,
modules,
titleOverride,
})}
category="contract_card"
label={contractId}
trackingProps={{
@@ -115,8 +161,6 @@ export const ContractCard: React.FC<ContractCardProps> = ({
<Badge className="border-[#a21caf] dark:border-[#86198f] text-white dark:bg-[linear-gradient(154deg,#4a044e,#2e1065)] bg-[linear-gradient(154deg,#d946ef,#9333ea)] py-[3px] px-[8px]">
Beta
</Badge>
) : isNewContract ? (
<Badge variant="outline">New</Badge>
) : null}
</div>
@@ -126,6 +170,7 @@ export const ContractCard: React.FC<ContractCardProps> = ({
className="inline-block"
skeletonData="Edition Drop"
loadedData={
titleOverride ||
publishedContractResult.data?.displayName ||
publishedContractResult.data?.name
}
@@ -140,7 +185,7 @@ export const ContractCard: React.FC<ContractCardProps> = ({
{publishedContractResult.data ? (
<p className="text-sm text-muted-foreground leading-5 mt-1">
{publishedContractResult.data?.description}
{descriptionOverride || publishedContractResult.data?.description}
</p>
) : (
<div className="mt-1">
@@ -149,8 +194,24 @@ export const ContractCard: React.FC<ContractCardProps> = ({
<Skeleton className="h-4 w-[60%]" />
</div>
)}
<div className="mt-auto pt-3 relative z-1 flex gap-2 justify-between">
{modules.length ? (
<div className="mt-auto pt-3 flex flex-row gap-1 flex-wrap">
{modules.slice(0, 2).map((m) => (
<Badge variant="outline" key={m.publisher + m.moduleId + m.version}>
{m.moduleId.split("ERC")[0]}
</Badge>
))}
{modules.length > 2 ? (
<Badge variant="outline">+{modules.length - 2}</Badge>
) : null}
</div>
) : null}
<div
className={cn(
"pt-3 relative z-1 flex gap-2 justify-between",
!modules?.length && "mt-auto",
)}
>
<ContractPublisher
addressOrEns={publishedContractResult.data?.publisher}
showSkeleton={showSkeleton}
@@ -163,7 +224,17 @@ export const ContractCard: React.FC<ContractCardProps> = ({
className="gap-1.5 py-1.5 px-2.5 text-xs h-auto relative z-10"
asChild
>
<Link href={`${href}/deploy`}>
<Link
href={getContractUrl(
{
publisher,
contractId,
version,
modules,
},
true,
)}
>
<RocketIcon className="size-3" />
Deploy
</Link>
@@ -48,17 +48,39 @@ export const ContractRow: React.FC<ContractRowProps> = ({ category }) => {
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 z-0 relative">
{category.contracts.slice(0, 6).map((publishedContractId, idx) => {
const [publisher, contractId] = publishedContractId.split("/");
const publisher: string = Array.isArray(publishedContractId)
? publishedContractId[0].split("/")[0]
: publishedContractId.split("/")[0];
const contractId: string = Array.isArray(publishedContractId)
? publishedContractId[0].split("/")[1]
: publishedContractId.split("/")[1];
const modules = Array.isArray(publishedContractId)
? publishedContractId[1]
: undefined;
const overrides = Array.isArray(publishedContractId)
? publishedContractId[2]
: undefined;
return (
<ContractCard
key={publishedContractId}
key={publisher + contractId + overrides?.title}
publisher={publisher}
contractId={contractId}
tracking={{
source: category.id,
itemIndex: `${idx}`,
}}
titleOverride={overrides?.title}
descriptionOverride={overrides?.description}
isBeta={category.isBeta}
modules={
modules?.length
? modules.map((m) => ({
publisher: m.split("/")[0],
moduleId: m.split("/")[1],
}))
: undefined
}
/>
);
})}
+64 -25
View File
@@ -8,7 +8,6 @@ import { useTrack } from "hooks/analytics/useTrack";
import { replaceIpfsUrl } from "lib/sdk";
import { ChevronsRightIcon } from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/router";
import { useMemo } from "react";
import { ContractDeployForm } from "../contract-components/contract-deploy-form";
@@ -19,7 +18,10 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { usePathname, useSearchParams } from "next/navigation";
import { Sidebar } from "../../@/components/blocks/Sidebar";
import { Badge } from "../../@/components/ui/badge";
import { useDashboardRouter } from "../../@/lib/DashboardRouter";
import { shareLink } from "../../@/lib/shareLink";
export interface PublishWithVersionPageProps {
@@ -44,7 +46,18 @@ export const PublishWithVersionPage: React.FC<PublishWithVersionPageProps> = ({
const availableVersions = allVersions.data?.map(({ version: v }) => v) || [];
const version = _version || availableVersions[0];
const trackEvent = useTrack();
const router = useRouter();
const router = useDashboardRouter();
const pathname = usePathname();
const searchparams = useSearchParams();
const modules = (searchparams?.getAll("module") || [])
.map((m) => JSON.parse(atob(m)))
.map((m) => ({
publisherAddress: m.publisher,
moduleName: m.moduleId,
moduleVersion: m.version || "latest",
}));
const stringifiedSearchParams = searchparams?.toString();
const publishedContract = useMemo(() => {
return (
@@ -78,9 +91,14 @@ export const PublishWithVersionPage: React.FC<PublishWithVersionPageProps> = ({
: `/${author}/${contractName}/${val}`;
if (isDeploy) {
router.push(`${pathName}/deploy`);
router.push(
`${pathName}/deploy${stringifiedSearchParams ? `?${stringifiedSearchParams}` : ""}`,
);
} else {
router.push(pathName);
router.push(
pathName +
(stringifiedSearchParams ? `?${stringifiedSearchParams}` : ""),
);
}
}
}}
@@ -104,28 +122,45 @@ export const PublishWithVersionPage: React.FC<PublishWithVersionPageProps> = ({
);
const contractInfo = (
<div className="flex gap-4 items-center flex-1">
{publishedContract?.logo && (
<div className="rounded-xl p-2 border border-border shrink-0 items-center justify-center hidden md:flex">
<img
className="size-12"
alt={publishedContract.name}
src={replaceIpfsUrl(publishedContract.logo)}
/>
</div>
)}
<div className="flex flex-col gap-2">
<div className="flex gap-4 items-center flex-1">
{publishedContract?.logo && (
<div className="rounded-xl p-2 border border-border shrink-0 items-center justify-center hidden md:flex">
<img
className="size-12"
alt={publishedContract.name}
src={replaceIpfsUrl(publishedContract.logo)}
/>
</div>
)}
<div className="flex flex-col gap-1">
<h1 className="text-2xl md:text-3xl font-bold tracking-tight">
{contractNameDisplay}
</h1>
<p className="text-muted-foreground text-sm">
{publishedContract?.description}
</p>
<div className="flex flex-col gap-1">
<h1 className="text-2xl md:text-3xl font-bold tracking-tight">
{contractNameDisplay}
</h1>
<p className="text-muted-foreground text-sm">
{publishedContract?.description}
</p>
</div>
</div>
</div>
);
const modulesList = modules.length ? (
<>
<div className="mt-auto flex flex-row gap-2 flex-wrap">
{modules.map((m) => (
<Badge
variant="default"
key={m.publisherAddress + m.moduleName + m.moduleVersion}
>
{m.moduleName.split("ERC")[0]}
</Badge>
))}
</div>
</>
) : null;
if (isDeploy) {
const showLoading = !deployContractId || !publishedContract;
return (
@@ -154,7 +189,7 @@ export const PublishWithVersionPage: React.FC<PublishWithVersionPageProps> = ({
{/* Right */}
<div className="flex-1">
{/* header */}
<div className="py-6 border-b border-border md:py-12">
<div className="py-6 flex flex-col gap-4 border-b border-border md:py-12">
<div className="flex flex-col md:flex-row justify-between gap-6 md:items-center">
{contractInfo}
<div className="flex flex-col-reverse md:flex-row gap-3">
@@ -162,7 +197,7 @@ export const PublishWithVersionPage: React.FC<PublishWithVersionPageProps> = ({
<div className="flex gap-3">
<Button asChild variant="outline">
<Link
href={`${router.asPath.replace("/deploy", "")}`}
href={`${pathname?.replace("/deploy", "") + (stringifiedSearchParams ? `?${stringifiedSearchParams}` : "")}`}
target="_blank"
>
Contract
@@ -182,6 +217,7 @@ export const PublishWithVersionPage: React.FC<PublishWithVersionPageProps> = ({
</div>
</div>
</div>
{modulesList}
</div>
{/* Content */}
@@ -205,19 +241,22 @@ export const PublishWithVersionPage: React.FC<PublishWithVersionPageProps> = ({
return (
<Flex direction="column" gap={{ base: 6, md: 10 }}>
{/* Header */}
<div className="py-2 md:pt-8 md:pb-0">
<div className="py-2 flex flex-col gap-4 md:pt-8 md:pb-0">
<div className="flex flex-col md:flex-row justify-between gap-6">
{contractInfo}
<div className="flex gap-3">
{versionSelector}
<Button asChild variant="primary" className="gap-2">
<Link href={`${router.asPath}/deploy`}>
<Link
href={`${pathname}/deploy${stringifiedSearchParams ? `?${stringifiedSearchParams}` : ""}`}
>
Deploy Now
<ChevronsRightIcon className="size-4" />
</Link>
</Button>
</div>
</div>
{modulesList}
</div>
{/* Content */}
@@ -76,7 +76,7 @@ export const ContractOverviewPage: React.FC<ContractOverviewPageProps> = ({
<BuildYourApp trackingCategory={TRACKING_CATEGORY} />
</GridItem>
<GridItem colSpan={{ xl: 3 }} as={Flex} direction="column" gap={6}>
<PublishedBy contractAddress={contract.address} />
<PublishedBy contract={contract} />
</GridItem>
</SimpleGrid>
);
+135 -33
View File
@@ -9,7 +9,15 @@ export interface ExploreCategory {
displayName?: string;
description: string;
learnMore?: string;
contracts: Readonly<PublishedContractID[]>;
contracts:
| Array<PublishedContractID>
| Array<
[
PublishedContractID,
Array<PublishedContractID>,
{ title: string; description: string },
]
>;
showInExplore?: boolean;
isBeta?: boolean;
}
@@ -27,7 +35,8 @@ const POPULAR = {
"thirdweb.eth/NFTStake",
"unlock-protocol.eth/PublicLock",
],
} as const;
} satisfies ExploreCategory;
const NFTS = {
id: "nft",
name: "NFT",
@@ -47,7 +56,7 @@ const NFTS = {
"thirdweb.eth/Multiwrap",
"kronickatz.eth/ERC721NESDrop",
],
} as const;
} satisfies ExploreCategory;
const GOVERNANCE = {
id: "daos-governance",
@@ -58,7 +67,7 @@ const GOVERNANCE = {
"thirdweb.eth/TokenERC20",
"thirdweb.eth/Split",
],
} as const;
} satisfies ExploreCategory;
const DROPS = {
id: "drops",
@@ -70,7 +79,7 @@ const DROPS = {
"thirdweb.eth/DropERC1155",
"thirdweb.eth/DropERC20",
],
} as const;
} satisfies ExploreCategory;
const MARKETS = {
id: "marketplace",
@@ -82,7 +91,109 @@ const MARKETS = {
"thirdweb.eth/TokenERC20",
"thirdweb.eth/Split",
],
} as const;
} satisfies ExploreCategory;
const MODULAR_CONTRACTS = {
id: "modular-contracts",
name: "modular",
displayName: "Modular Contracts (Beta)",
description:
"Collection of highly customizable and upgradeable smart contracts built with the modular contracts framework.",
isBeta: true,
contracts: [
// erc721 drop
[
"thirdweb.eth/ERC721CoreInitializable",
[
"deployer.thirdweb.eth/ClaimableERC721",
"deployer.thirdweb.eth/BatchMetadataERC721",
"deployer.thirdweb.eth/RoyaltyERC721",
],
{
title: "Modular NFT Drop",
description: "ERC721 NFTs that anyone can mint.",
},
],
// erc721 token
[
"thirdweb.eth/ERC721CoreInitializable",
[
"deployer.thirdweb.eth/MintableERC721",
"deployer.thirdweb.eth/BatchMetadataERC721",
"deployer.thirdweb.eth/TransferableERC721",
],
{
title: "Modular NFT Collection",
description: "ERC721 NFTs that only owners can mint.",
},
],
// open edition 721
[
"thirdweb.eth/ERC721CoreInitializable",
[
"deployer.thirdweb.eth/ClaimableERC721",
"deployer.thirdweb.eth/OpenEditionMetadataERC721",
"deployer.thirdweb.eth/RoyaltyERC721",
],
{
title: "Modular Open Edition",
description: "ERC721 NFTs with identical metadata.",
},
],
// erc1155 drop
[
"thirdweb.eth/ERC1155CoreInitializable",
[
"deployer.thirdweb.eth/ClaimableERC1155",
"deployer.thirdweb.eth/BatchMetadataERC1155",
"deployer.thirdweb.eth/RoyaltyERC1155",
"deployer.thirdweb.eth/SequentialTokenIdERC1155",
],
{
title: "Modular Edition Drop",
description: "ERC1155 NFTs that others can mint.",
},
],
// erc1155 token
[
"thirdweb.eth/ERC1155CoreInitializable",
[
"deployer.thirdweb.eth/MintableERC1155",
"deployer.thirdweb.eth/BatchMetadataERC1155",
"deployer.thirdweb.eth/TransferableERC1155",
"deployer.thirdweb.eth/SequentialTokenIdERC1155",
],
{
title: "Modular Edition",
description: "ERC1155 NFTs that only owners can mint.",
},
],
// erc20 drop
[
"thirdweb.eth/ERC20CoreInitializable",
[
"deployer.thirdweb.eth/ClaimableERC20",
"deployer.thirdweb.eth/TransferableERC20",
],
{
title: "Modular Token Drop",
description: "ERC20 Tokens that others can mint.",
},
],
// erc20 token
[
"thirdweb.eth/ERC20CoreInitializable",
[
"deployer.thirdweb.eth/MintableERC20",
"deployer.thirdweb.eth/TransferableERC20",
],
{
title: "Modular Token",
description: "ERC20 Tokens that only owners can mint.",
},
],
],
} satisfies ExploreCategory;
const AIRDROP = {
id: "airdrop",
@@ -91,7 +202,7 @@ const AIRDROP = {
description:
"Efficiently transfer large numbers of on-chain assets to a large number of recipients.",
contracts: ["thirdweb.eth/Airdrop"],
} as const;
} satisfies ExploreCategory;
const GAMING = {
id: "gaming",
@@ -109,7 +220,7 @@ const GAMING = {
"thirdweb.eth/NFTStake",
],
showInExplore: false,
} as const;
} satisfies ExploreCategory;
const LOYALTY = {
id: "loyalty",
@@ -123,7 +234,7 @@ const LOYALTY = {
"thirdweb.eth/TokenERC20",
],
showInExplore: false,
} as const;
} satisfies ExploreCategory;
const COMMERCE = {
id: "commerce",
@@ -138,7 +249,7 @@ const COMMERCE = {
"thirdweb.eth/Split",
],
showInExplore: false,
} as const;
} satisfies ExploreCategory;
const STAKING = {
id: "staking",
@@ -151,7 +262,7 @@ const STAKING = {
"thirdweb.eth/TokenStake",
],
showInExplore: true,
} as const;
} satisfies ExploreCategory;
const SMART_WALLET = {
id: "smart-wallet",
@@ -166,10 +277,11 @@ const SMART_WALLET = {
"thirdweb.eth/ManagedAccountFactory",
],
showInExplore: true,
} as const;
} satisfies ExploreCategory;
const CATEGORIES = {
const CATEGORIES: Record<string, ExploreCategory> = {
[POPULAR.id]: POPULAR,
[MODULAR_CONTRACTS.id]: MODULAR_CONTRACTS,
[NFTS.id]: NFTS,
[MARKETS.id]: MARKETS,
[DROPS.id]: DROPS,
@@ -180,9 +292,9 @@ const CATEGORIES = {
[COMMERCE.id]: COMMERCE,
[STAKING.id]: STAKING,
[GOVERNANCE.id]: GOVERNANCE,
} as const;
};
export function getCategory(id: string): ExploreCategory | null {
export function getCategory(id: string) {
if (isExploreCategory(id)) {
return CATEGORIES[id];
}
@@ -195,8 +307,8 @@ function isExploreCategory(category: string): category is ExploreCategoryName {
return category in CATEGORIES;
}
export const EXPLORE_PAGE_DATA = Object.values(CATEGORIES).filter(
(v) => (v as ExploreCategory).showInExplore !== false,
export const EXPLORE_PAGE_DATA = Object.values(CATEGORIES).filter((v) =>
"showInExplore" in v ? v.showInExplore !== false : true,
);
export const ALL_CATEGORIES = Object.values(CATEGORIES).map((v) => v.id);
@@ -208,23 +320,13 @@ export function prefetchCategory(
return Promise.allSettled(
category.contracts.map((contract) =>
queryClient.fetchQuery(
publishedContractQuery(`${contract}/latest`, queryClient),
publishedContractQuery(
Array.isArray(contract)
? `${contract[0]}/latest`
: `${contract}/latest`,
queryClient,
),
),
),
);
}
export function getAllExplorePublishedContracts() {
const all = EXPLORE_PAGE_DATA.flatMap((category) => category.contracts);
return [...new Set(all)];
}
export function getAllExplorePublishers() {
return [
...new Set(
getAllExplorePublishedContracts().map(
(contract) => contract.split("/")[0],
),
),
];
}
@@ -23,7 +23,6 @@ import type {
InferGetStaticPropsType,
} from "next";
import { NextSeo } from "next-seo";
// import dynamic from "next/dynamic";
import { PageId } from "page-id";
import { FiChevronLeft } from "react-icons/fi";
import { Heading, Link, Text } from "tw-components";
@@ -93,17 +92,38 @@ const ExploreCategoryPage: ThirdwebNextPage = (
</Flex>
<SimpleGrid columns={{ base: 1, md: 3 }} gap={5}>
{props.category.contracts.map((publishedContractId, idx) => {
const [publisher, contractId] = publishedContractId.split("/");
const publisher: string = Array.isArray(publishedContractId)
? publishedContractId[0].split("/")[0]
: publishedContractId.split("/")[0];
const contractId: string = Array.isArray(publishedContractId)
? publishedContractId[0].split("/")[1]
: publishedContractId.split("/")[1];
const modules = Array.isArray(publishedContractId)
? publishedContractId[1]
: undefined;
const overrides = Array.isArray(publishedContractId)
? publishedContractId[2]
: undefined;
return (
<ContractCard
key={publishedContractId}
key={publisher + contractId + overrides?.title}
publisher={publisher}
contractId={contractId}
titleOverride={overrides?.title}
descriptionOverride={overrides?.description}
tracking={{
source: props.category.id,
itemIndex: `${idx}`,
}}
isBeta={props.category.isBeta}
modules={
modules?.length
? modules.map((m) => ({
publisher: m.split("/")[0],
moduleId: m.split("/")[1],
}))
: undefined
}
/>
);
})}