[Update] Refactor contract publish form to use thirdweb v5 (#4605)

<!-- start pr-codex -->

## PR-Codex overview
This PR removes unnecessary files, updates type definitions, refactors contract-related components, and improves API handling.

### Detailed summary
- Removed several files related to publishing contracts and provider setup
- Updated type definitions and constants
- Refactored contract-related components for consistency
- Improved API handling and URL configurations

> The following files were skipped due to too many changes: `apps/dashboard/src/pages/explore/[category]/index.tsx`, `apps/dashboard/src/contract-ui/tabs/claim-conditions/components/claim-conditions-form/hooks.ts`, `apps/dashboard/src/app/(dashboard)/published-contract/[publisher]/[contract_id]/page.tsx`, `apps/dashboard/src/contract-ui/tabs/manage/components/moduleContractInfo.ts`, `apps/dashboard/src/constants/chains.ts`, `apps/dashboard/src/components/contract-components/contract-table/cells/description.tsx`, `apps/dashboard/src/contract-ui/tabs/claim-conditions/components/claim-conditions-form/index.tsx`, `apps/dashboard/src/components/contract-components/contract-publish-form/landing-fieldset.tsx`, `apps/dashboard/src/components/contract-components/contract-publish-form/factory-fieldset.tsx`, `apps/dashboard/src/app/(dashboard)/contracts/publish/[publish_uri]/page.tsx`, `apps/dashboard/src/components/explore/contract-card/index.tsx`, `apps/dashboard/src/components/contract-components/contract-publish-form/default-factory.tsx`, `apps/dashboard/src/components/contract-components/shared/contract-id-image.tsx`, `apps/dashboard/src/components/smart-wallets/AccountFactories/index.tsx`, `apps/dashboard/src/lib/sdk.ts`, `apps/dashboard/src/contract-ui/tabs/code/components/code-overview.tsx`, `apps/dashboard/src/components/contract-components/publisher/edit-profile.tsx`, `apps/dashboard/src/pages/profile/[profileAddress].tsx`, `apps/dashboard/src/contract-ui/tabs/claim-conditions/legacy-zod-schema.ts`, `apps/dashboard/src/contract-ui/tabs/manage/components/ModuleForm.tsx`, `apps/dashboard/src/components/contract-functions/contract-function.tsx`, `apps/dashboard/src/components/contract-components/hooks.ts`, `apps/dashboard/src/components/contract-components/contract-publish-form/index.tsx`

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

<!-- end pr-codex -->
This commit is contained in:
jnsdls
2024-09-16 19:33:35 +00:00
parent 468067e47f
commit 95b3c787a3
57 changed files with 652 additions and 1734 deletions
-25
View File
@@ -115,31 +115,6 @@ module.exports = {
},
},
overrides: [
{
files: "src/core-ui/**/*",
rules: {
// no restricted imports
"@typescript-eslint/no-restricted-imports": [
"error",
{
paths: [
{
name: "@thirdweb-dev/sdk",
message:
"core-ui should not import from @thirdweb-dev/sdk. (except for types)",
allowTypeImports: true,
},
{
name: "@thirdweb-dev/react",
message:
"core-ui should not import from @thirdweb-dev/react. (except for types)",
allowTypeImports: true,
},
],
},
],
},
},
// disable restricted imports in tw-components
{
files: "src/tw-components/**/*",
+1 -7
View File
@@ -4,11 +4,5 @@
"ignore": ["src/@/components/ui/**"],
"project": ["src/**"],
"ignoreBinaries": ["only-allow", "knip", "biome"],
"ignoreDependencies": [
"@storybook/blocks",
"@storybook/test",
"@thirdweb-dev/sdk",
"@thirdweb-dev/storage",
"@thirdweb-dev/service-utils"
]
"ignoreDependencies": ["@storybook/blocks", "@thirdweb-dev/service-utils"]
}
-2
View File
@@ -1,7 +1,6 @@
// @ts-check
/**
*
* @returns {Promise<import("@thirdweb-dev/chains").Chain[]>}
*/
async function fetchChainsFromApi() {
const res = await fetch(`https://api.thirdweb.com/v1/chains`, {
@@ -21,7 +20,6 @@ async function fetchChainsFromApi() {
/**
*
* @param {number|string} chainIdOrSlug
* @returns {Promise<import("@thirdweb-dev/chains").Chain>}
*/
async function getSingleChain(chainIdOrSlug) {
const res = await fetch(
-3
View File
@@ -52,9 +52,7 @@
"@stripe/stripe-js": "^3.5.0",
"@tanstack/react-query": "5.56.2",
"@tanstack/react-table": "^8.17.3",
"@thirdweb-dev/sdk": "workspace:*",
"@thirdweb-dev/service-utils": "workspace:*",
"@thirdweb-dev/storage": "workspace:*",
"@vercel/og": "^0.6.2",
"abitype": "1.0.5",
"chakra-react-select": "^4.7.6",
@@ -63,7 +61,6 @@
"color": "^4.2.3",
"compare-versions": "^6.1.0",
"date-fns": "^3.6.0",
"ethers": "5.7.2",
"flat": "^6.0.1",
"framer-motion": "11.5.4",
"fuse.js": "7.0.0",
+2
View File
@@ -6,6 +6,8 @@ const LOGGED_IN_ONLY_PATHS = [
// anything that _starts_ with /cli is logged in only
"/cli",
"/support",
// publish page
"/contracts/publish",
];
export function isLoginRequired(pathname: string) {
+4
View File
@@ -16,3 +16,7 @@ export const isProd =
"production";
export const PROD_OR_DEV_URL = isProd ? "thirdweb.com" : "thirdweb-dev.com";
export const DASHBOARD_STORAGE_URL =
process.env.NEXT_PUBLIC_DASHBOARD_UPLOAD_SERVER ||
"https://storage.thirdweb.com";
@@ -15,7 +15,7 @@ import { remove } from "thirdweb/extensions/thirdweb";
import { useActiveAccount } from "thirdweb/react";
import invariant from "tiny-invariant";
function useMultiChainRegContractList(walletAddress?: string) {
export function useMultiChainRegContractList(walletAddress?: string) {
return useQuery({
queryKey: ["dashboard-registry", walletAddress, "multichain-contract-list"],
queryFn: async () => {
@@ -0,0 +1,47 @@
import { ChakraProviderSetup } from "@/components/ChakraProviderSetup";
import { thirdwebClient } from "@/constants/client";
import { ContractPublishForm } from "components/contract-components/contract-publish-form";
import { setOverrides } from "lib/vercel-utils";
import { revalidatePath } from "next/cache";
import { fetchDeployMetadata } from "thirdweb/contract";
setOverrides();
type DirectDeployPageProps = {
params: {
publish_uri: string;
};
};
export default async function PublishContractPage(
props: DirectDeployPageProps,
) {
const decodedPublishUri = decodeURIComponent(props.params.publish_uri);
const publishUri = decodedPublishUri.startsWith("ipfs://")
? decodedPublishUri
: `ipfs://${decodedPublishUri}`;
const publishMetadata = await fetchDeployMetadata({
uri: publishUri,
client: thirdwebClient,
});
return (
<div className="container py-8 flex flex-col gap-8">
<ChakraProviderSetup>
<ContractPublishForm
publishMetadata={publishMetadata}
onPublishSuccess={async () => {
"use server";
// we are pretty brutal here and simply invalidate ALL published contracts (for everyone!) and versions no matter what
// TODO: we can be more granular here and only invalidate the specific contract and version etc
revalidatePath(
"/(dashboard)/published-contract/[publisher]/[contract_id]",
"layout",
);
}}
/>
</ChakraProviderSetup>
</div>
);
}
@@ -1,5 +1,4 @@
import { Separator } from "@/components/ui/separator";
import { SimpleGrid } from "@chakra-ui/react";
import { ChakraProviderSetup } from "../../../../../@/components/ChakraProviderSetup";
import { PublishedContract } from "../../../../../components/contract-components/published-contract";
import { setOverrides } from "../../../../../lib/vercel-utils";
@@ -42,12 +41,12 @@ export default async function PublishedContractPage(
<Separator />
{/* TODO: remove the chakra things :) */}
<ChakraProviderSetup>
<SimpleGrid columns={12} gap={{ base: 6, md: 10 }} w="full">
<div className="grid grid-cols-12 gap-6 md:gap-10 w-full">
<PublishedContract
publishedContract={publishedContract}
walletOrEns={props.params.publisher}
/>
</SimpleGrid>
</div>
</ChakraProviderSetup>
</>
);
@@ -1,48 +0,0 @@
import { thirdwebClient } from "@/constants/client";
import type { Signer } from "ethers";
import * as ethers from "ethers";
import { useEffect, useState } from "react";
import { toEthersSigner } from "thirdweb/adapters/ethers5";
import {
useActiveAccount,
useActiveWallet,
useActiveWalletChain,
} from "thirdweb/react";
export function useEthersSigner() {
const activeWallet = useActiveWallet();
const activeAccount = useActiveAccount();
const activeChain = useActiveWalletChain();
const [signer, setSigner] = useState<Signer | undefined>(undefined);
// will be deleted as part of: https://github.com/thirdweb-dev/dashboard/pull/2648
// eslint-disable-next-line no-restricted-syntax
useEffect(() => {
let active = true;
async function run() {
if (!activeWallet || !activeAccount || !activeChain) {
setSigner(undefined);
return;
}
try {
const s = await toEthersSigner(
ethers,
thirdwebClient,
activeAccount,
activeChain,
);
if (active) {
setSigner(s);
}
} catch (e) {
console.error("failed to get signer", e);
}
}
run();
return () => {
active = false;
};
}, [activeAccount, activeChain, activeWallet]);
return signer;
}
@@ -1,28 +1,15 @@
import { Flex, FormControl, ListItem, UnorderedList } from "@chakra-ui/react";
import type { Abi, AbiParameter } from "abitype";
import type { Abi } from "abitype";
import { useFormContext } from "react-hook-form";
import { Heading, Link, Text } from "tw-components";
import { AbiSelector } from "./abi-selector";
import { DynamicContractsFieldset } from "./dynamic-contract-fieldset";
import { ModularContractsFieldset } from "./modular-contract-fieldset";
import { ModulesParamSelector } from "./modules-param-selector";
import { NetworksFieldset } from "./networks-fieldset";
interface DefaultFactoryProps {
abi: Abi;
shouldShowDynamicFactoryInput: boolean;
shouldShowModularFactoryInput: boolean;
shouldShowModulesParamInput: boolean;
deployParams: readonly AbiParameter[];
}
export const DefaultFactory: React.FC<DefaultFactoryProps> = ({
abi,
shouldShowDynamicFactoryInput,
shouldShowModularFactoryInput,
shouldShowModulesParamInput,
deployParams,
}) => {
export const DefaultFactory: React.FC<DefaultFactoryProps> = ({ abi }) => {
const form = useFormContext();
return (
@@ -74,36 +61,8 @@ export const DefaultFactory: React.FC<DefaultFactoryProps> = ({
}
/>
</FormControl>
{shouldShowModulesParamInput && (
<>
<Flex flexDir="column" gap={2} mt={4}>
<Heading size="title.md">Modules Param</Heading>
<Text>
The contract uses modules. Choose the modules param name from
the initializer params below.
</Text>
</Flex>
<FormControl isRequired>
<ModulesParamSelector
deployParams={deployParams}
value={form.watch(
"factoryDeploymentData.modularFactoryInput.modulesParamName",
)}
onChange={(selectedParam) =>
form.setValue(
"factoryDeploymentData.modularFactoryInput.modulesParamName",
selectedParam,
)
}
/>
</FormControl>
</>
)}
</Flex>
<NetworksFieldset />
{shouldShowDynamicFactoryInput && <DynamicContractsFieldset />}
{shouldShowModularFactoryInput && <ModularContractsFieldset />}
</Flex>
);
};
@@ -1,46 +0,0 @@
import { Box, Flex, Icon } from "@chakra-ui/react";
import { useFieldArray, useFormContext } from "react-hook-form";
import { FiPlus } from "react-icons/fi";
import { Button, Heading, Text } from "tw-components";
import { ExtensionInput } from "./extension-input";
export const DynamicContractsFieldset = () => {
const form = useFormContext();
const { fields, append, remove } = useFieldArray({
name: "defaultExtensions",
control: form.control,
});
return (
<Flex gap={8} direction="column" as="fieldset">
<Flex gap={2} direction="column">
<Heading size="title.md">Contract extension settings</Heading>
<Text>You can set default extensions for your contract.</Text>
</Flex>
<Flex flexDir="column" gap={4}>
{fields.map((item, index) => (
<ExtensionInput key={item.id} remove={remove} index={index} />
))}
<Box>
<Button
type="button"
size="sm"
colorScheme="primary"
borderRadius="md"
leftIcon={<Icon as={FiPlus} />}
onClick={() =>
append({
extensionName: "",
extensionVersion: "",
publisherAddress: "",
})
}
>
Add Extension
</Button>
</Box>
</Flex>
</Flex>
);
};
@@ -1,119 +0,0 @@
import {
Divider,
Flex,
FormControl,
Icon,
IconButton,
Input,
Select,
Skeleton,
} from "@chakra-ui/react";
import { useFormContext } from "react-hook-form";
import { FiTrash } from "react-icons/fi";
import { FormLabel } from "tw-components";
import { useAllVersions, usePublishedContractsQuery } from "../hooks";
interface ExtensionInputProps {
index: number;
remove: (index: number) => void;
}
export const ExtensionInput: React.FC<ExtensionInputProps> = ({
index,
remove,
}) => {
const form = useFormContext();
const publishedContractsQuery = usePublishedContractsQuery(
form.watch(`defaultExtensions.${index}.publisherAddress`),
);
const allVersions = useAllVersions(
form.watch(`defaultExtensions.${index}.publisherAddress`),
form.watch(`defaultExtensions.${index}.extensionName`),
);
return (
<Flex flexDir="column" gap={2}>
<Flex
w="full"
gap={{ base: 4, md: 2 }}
flexDir={{ base: "column", md: "row" }}
>
<FormControl
as={Flex}
flexDir="column"
gap={1}
isInvalid={
!!form.getFieldState(
`defaultExtensions.${index}.publisherAddress`,
form.formState,
).error
}
>
<FormLabel textTransform="capitalize">Publisher</FormLabel>
<Input
placeholder="Address or ENS"
{...form.register(`defaultExtensions.${index}.publisherAddress`)}
/>
</FormControl>
<FormControl as={Flex} flexDir="column" gap={1}>
<FormLabel textTransform="capitalize">Extension Name</FormLabel>
<Skeleton
isLoaded={
!!publishedContractsQuery.data ||
!publishedContractsQuery.isFetching
}
borderRadius="lg"
>
<Select
isDisabled={(publishedContractsQuery?.data || []).length === 0}
{...form.register(`defaultExtensions.${index}.extensionName`)}
placeholder={
publishedContractsQuery.isFetched &&
(publishedContractsQuery?.data || []).length === 0
? "No extensions found"
: "Select extension"
}
>
{publishedContractsQuery?.data?.map(({ id }) => (
<option key={id} value={id}>
{id}
</option>
))}
</Select>
</Skeleton>
</FormControl>
<FormControl as={Flex} flexDir="column" gap={1}>
<FormLabel textTransform="capitalize">Extension Version</FormLabel>
<Skeleton
isLoaded={!!allVersions.data || !allVersions.isFetching}
borderRadius="lg"
>
<Select
w="full"
isDisabled={!allVersions.data}
{...form.register(`defaultExtensions.${index}.extensionVersion`)}
borderRadius="lg"
>
<option value="">Always latest</option>
{allVersions?.data?.map(({ version }) => (
<option key={version} value={version}>
{version}
</option>
))}
</Select>
</Skeleton>
</FormControl>
<IconButton
icon={<Icon as={FiTrash} boxSize={5} />}
aria-label="Remove row"
onClick={() => remove(index)}
alignSelf="end"
/>
</Flex>
<Divider />
</Flex>
);
};
@@ -1,5 +1,5 @@
import { ButtonGroup, Flex } from "@chakra-ui/react";
import type { Abi, AbiParameter } from "abitype";
import type { Abi } from "abitype";
import type { Dispatch, SetStateAction } from "react";
import { useFormContext } from "react-hook-form";
import { Button, Heading } from "tw-components";
@@ -9,19 +9,11 @@ import { DefaultFactory } from "./default-factory";
interface FactoryFieldsetProps {
abi: Abi;
setCustomFactoryAbi: Dispatch<SetStateAction<Abi>>;
shouldShowDynamicFactoryInput: boolean;
shouldShowModularFactoryInput: boolean;
shouldShowModulesParamInput: boolean;
deployParams: readonly AbiParameter[];
}
export const FactoryFieldset: React.FC<FactoryFieldsetProps> = ({
abi,
setCustomFactoryAbi,
shouldShowDynamicFactoryInput,
shouldShowModularFactoryInput,
shouldShowModulesParamInput,
deployParams,
}) => {
const form = useFormContext();
@@ -56,13 +48,7 @@ export const FactoryFieldset: React.FC<FactoryFieldsetProps> = ({
</Button>
</ButtonGroup>
{form.watch("deployType") === "autoFactory" && (
<DefaultFactory
abi={abi}
shouldShowDynamicFactoryInput={shouldShowDynamicFactoryInput}
shouldShowModularFactoryInput={shouldShowModularFactoryInput}
shouldShowModulesParamInput={shouldShowModulesParamInput}
deployParams={deployParams}
/>
<DefaultFactory abi={abi} />
)}
{form.watch("deployType") === "customFactory" && (
<CustomFactory setCustomFactoryAbi={setCustomFactoryAbi} />
@@ -1,76 +1,61 @@
"use client";
import { thirdwebClient } from "@/constants/client";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import { useIsomorphicLayoutEffect } from "@/lib/useIsomorphicLayoutEffect";
import { CustomConnectWallet } from "@3rdweb-sdk/react/components/connect-wallet";
import { useLoggedInUser } from "@3rdweb-sdk/react/hooks/useLoggedInUser";
import { Box, Divider, Flex, Icon, IconButton } from "@chakra-ui/react";
import {
CONTRACT_ADDRESSES,
ExtraPublishMetadataSchemaInput,
detectFeatures,
isExtensionEnabled,
} from "@thirdweb-dev/sdk";
import type { Abi } from "abitype";
import { defaultChains } from "constants/chains";
import {
DASHBOARD_ENGINE_RELAYER_URL,
DASHBOARD_FORWARDER_ADDRESS,
} from "constants/misc";
import { useTrack } from "hooks/analytics/useTrack";
import { useTxNotifications } from "hooks/useTxNotifications";
import { useRouter } from "next/router";
import { useEffect, useMemo, useRef, useState } from "react";
import { useMemo, useState } from "react";
import { FormProvider, useForm } from "react-hook-form";
import { IoChevronBack } from "react-icons/io5";
import { useActiveAccount } from "thirdweb/react";
import { Button, Text } from "tw-components";
import { z } from "zod";
import type { FetchDeployMetadataResult } from "thirdweb/contract";
import {
useContractFullPublishMetadata,
useContractPrePublishMetadata,
useContractPublishMetadataFromURI,
useEns,
useFunctionParamsFromABI,
usePublishMutation,
} from "../hooks";
import type { ContractId } from "../types";
getContractPublisher,
publishContract,
} from "thirdweb/extensions/thirdweb";
import { useActiveAccount, useSendAndConfirmTransaction } from "thirdweb/react";
import { Button, Text } from "tw-components";
import { useEns, useFunctionParamsFromABI } from "../hooks";
import { ContractParamsFieldset } from "./contract-params-fieldset";
import { FactoryFieldset } from "./factory-fieldset";
import { LandingFieldset } from "./landing-fieldset";
import { NetworksFieldset } from "./networks-fieldset";
const ExtraPublishMetadataSchema = ExtraPublishMetadataSchemaInput.extend({
customFactoryAddresses: z.array(
z.object({
key: z.number(),
value: z.string(),
}),
),
});
interface ContractPublishFormProps {
contractId: ContractId;
}
export const ContractPublishForm: React.FC<ContractPublishFormProps> = ({
contractId,
}) => {
export function ContractPublishForm(props: {
publishMetadata: FetchDeployMetadataResult;
onPublishSuccess: () => Promise<void>;
}) {
useLoggedInUser();
const [customFactoryAbi, setCustomFactoryAbi] = useState<Abi>([]);
const configuredChainsIds = defaultChains.map((c) => c.id);
const [fieldsetToShow, setFieldsetToShow] = useState<
"landing" | "factory" | "contractParams" | "networks"
>("landing");
const trackEvent = useTrack();
const router = useRouter();
const router = useDashboardRouter();
const { onSuccess, onError } = useTxNotifications(
"Successfully published contract",
"Failed to publish contract",
);
const address = useActiveAccount()?.address;
const publishMutation = usePublishMutation();
const account = useActiveAccount();
const sendTx = useSendAndConfirmTransaction({
gasless: {
experimentalChainlessSupport: true,
provider: "engine",
relayerUrl: DASHBOARD_ENGINE_RELAYER_URL,
relayerForwarderAddress: DASHBOARD_FORWARDER_ADDRESS,
},
});
const publishMetadata = useContractPublishMetadataFromURI(contractId);
const prePublishMetadata = useContractPrePublishMetadata(contractId, address);
const latestVersion =
prePublishMetadata.data?.latestPublishedContractMetadata?.publishedMetadata
.version;
const latestVersion = props.publishMetadata.version;
const placeholderVersion = useMemo(() => {
if (latestVersion) {
@@ -82,89 +67,53 @@ export const ContractPublishForm: React.FC<ContractPublishFormProps> = ({
const transformedQueryData = useMemo(() => {
return {
...prePublishMetadata.data?.latestPublishedContractMetadata
?.publishedMetadata,
...props.publishMetadata,
name: props.publishMetadata.name || "",
changelog: "",
version: placeholderVersion,
displayName:
prePublishMetadata.data?.latestPublishedContractMetadata
?.publishedMetadata.displayName ||
prePublishMetadata.data?.preDeployMetadata.info.title ||
publishMetadata.data?.name ||
props.publishMetadata.displayName ||
props.publishMetadata.info?.title ||
props.publishMetadata.name ||
"",
description:
prePublishMetadata.data?.latestPublishedContractMetadata
?.publishedMetadata.description ||
prePublishMetadata.data?.preDeployMetadata.info.notice ||
props.publishMetadata.description ||
props.publishMetadata.info?.notice ||
"",
factoryDeploymentData: prePublishMetadata.data
?.latestPublishedContractMetadata?.publishedMetadata
?.factoryDeploymentData || {
factoryAddresses: Object.fromEntries(
configuredChainsIds
.map((id) =>
id in CONTRACT_ADDRESSES
? [
id,
CONTRACT_ADDRESSES[id as keyof typeof CONTRACT_ADDRESSES]
.twFactory,
]
: null,
)
.filter(Boolean) as [number, string][],
),
implementationAddresses: Object.fromEntries(
configuredChainsIds.map((id) => [id, ""]),
),
factoryDeploymentData: props.publishMetadata?.factoryDeploymentData || {
factoryAddresses: {},
implementationAddresses: {},
implementationInitializerFunction: "initialize",
customFactoryInput: {
factoryFunction:
prePublishMetadata.data?.latestPublishedContractMetadata
?.publishedMetadata.factoryDeploymentData?.customFactoryInput
props.publishMetadata.factoryDeploymentData?.customFactoryInput
?.factoryFunction || "deployProxyByImplementation",
customFactoryAddresses:
prePublishMetadata.data?.latestPublishedContractMetadata
?.publishedMetadata?.factoryDeploymentData?.customFactoryInput
props.publishMetadata.factoryDeploymentData?.customFactoryInput
?.customFactoryAddresses || {},
params: props.publishMetadata.customFactoryInput?.params || [],
},
},
constructorParams:
prePublishMetadata.data?.latestPublishedContractMetadata
?.publishedMetadata?.constructorParams || {},
networksForDeployment: prePublishMetadata.data
?.latestPublishedContractMetadata?.publishedMetadata
.networksForDeployment || {
constructorParams: props.publishMetadata.constructorParams || {},
networksForDeployment: props.publishMetadata?.networksForDeployment || {
allNetworks: true,
},
deployType:
prePublishMetadata.data?.latestPublishedContractMetadata
?.publishedMetadata?.deployType || "standard",
deployType: props.publishMetadata.deployType || "standard",
customFactoryAddresses: Object.entries(
prePublishMetadata.data?.latestPublishedContractMetadata
?.publishedMetadata?.factoryDeploymentData?.customFactoryInput
props.publishMetadata.factoryDeploymentData?.customFactoryInput
?.customFactoryAddresses || {},
).map(([key, value]) => ({ key: Number(key), value })),
defaultExtensions:
prePublishMetadata.data?.latestPublishedContractMetadata
?.publishedMetadata?.defaultExtensions || [],
defaultModules:
prePublishMetadata.data?.latestPublishedContractMetadata
?.publishedMetadata?.defaultModules || [],
defaultExtensions: props.publishMetadata.defaultExtensions || [],
defaultModules: props.publishMetadata.defaultModules || [],
extensionsParamName:
prePublishMetadata.data?.latestPublishedContractMetadata
?.publishedMetadata.factoryDeploymentData?.modularFactoryInput
?.extensionsParamName || "",
props.publishMetadata.factoryDeploymentData?.modularFactoryInput
?.hooksParamName || "",
};
}, [
configuredChainsIds,
placeholderVersion,
prePublishMetadata.data?.latestPublishedContractMetadata?.publishedMetadata,
prePublishMetadata.data?.preDeployMetadata.info.notice,
prePublishMetadata.data?.preDeployMetadata.info.title,
publishMetadata.data?.name,
]);
}, [placeholderVersion, props.publishMetadata]);
const form = useForm<z.input<typeof ExtraPublishMetadataSchema>>({
const form = useForm<
FetchDeployMetadataResult & { name: string; version: string }
>({
defaultValues: transformedQueryData,
values: transformedQueryData,
resetOptions: {
@@ -173,61 +122,46 @@ export const ContractPublishForm: React.FC<ContractPublishFormProps> = ({
},
});
const hasTrackedImpression = useRef<boolean>(false);
// TODO: figure out a better way to track impressions
// eslint-disable-next-line no-restricted-syntax
useEffect(() => {
if (publishMetadata.data && !hasTrackedImpression.current) {
hasTrackedImpression.current = true;
trackEvent({
action: "impression",
category: "publish",
analytics: publishMetadata.data.analytics,
});
}
}, [publishMetadata.data, trackEvent]);
const disableNext =
!form.watch("version") ||
!form.watch("displayName") ||
!!form.getFieldState("version", form.formState).error;
const ensQuery = useEns(address);
const ensQuery = useEns(account?.address);
const ensNameOrAddress = useMemo(() => {
return ensQuery?.data?.ensName || ensQuery.data?.address;
}, [ensQuery.data]);
const successRedirectUrl = useMemo(() => {
if (!ensNameOrAddress || !publishMetadata.data?.name) {
if (!ensNameOrAddress || !props.publishMetadata.name) {
return undefined;
}
return `/${ensNameOrAddress}/${publishMetadata.data.name}`;
}, [ensNameOrAddress, publishMetadata.data?.name]);
return `/${ensNameOrAddress}/${props.publishMetadata.name}`;
}, [ensNameOrAddress, props.publishMetadata.name]);
const isDisabled = useMemo(
() => !successRedirectUrl || !address,
[successRedirectUrl, address],
() => !successRedirectUrl || !account,
[successRedirectUrl, account],
);
const fullPublishMetadata = useContractFullPublishMetadata(contractId);
const constructorParams =
publishMetadata.data?.abi?.find((c) => c.type === "constructor")?.inputs ||
props.publishMetadata.abi.find((c) => c.type === "constructor")?.inputs ||
[];
const initializerParams = useFunctionParamsFromABI(
(form.watch("deployType") === "customFactory"
form.watch("deployType") === "customFactory"
? customFactoryAbi
: publishMetadata.data?.abi) as Abi,
: props.publishMetadata.abi,
form.watch("deployType") === "customFactory"
? form.watch(
"factoryDeploymentData.customFactoryInput.factoryFunction",
) ||
fullPublishMetadata.data?.factoryDeploymentData?.customFactoryInput
props.publishMetadata.factoryDeploymentData?.customFactoryInput
?.factoryFunction ||
"deployProxyByImplementation"
: form.watch("factoryDeploymentData.implementationInitializerFunction") ||
fullPublishMetadata.data?.factoryDeploymentData
props.publishMetadata.factoryDeploymentData
?.implementationInitializerFunction ||
"initialize",
);
@@ -237,66 +171,8 @@ export const ContractPublishForm: React.FC<ContractPublishFormProps> = ({
? constructorParams
: initializerParams;
const extensions = detectFeatures(publishMetadata.data?.abi || []);
const isPluginRouter = useMemo(
() =>
isExtensionEnabled(
publishMetadata.data?.abi || [],
"PluginRouter",
extensions,
),
[publishMetadata.data?.abi, extensions],
);
const isDynamicContract = useMemo(
() =>
isExtensionEnabled(
publishMetadata.data?.abi || [],
"DynamicContract",
extensions,
),
[publishMetadata.data?.abi, extensions],
);
const isModularContract = useMemo(
() =>
isExtensionEnabled(
publishMetadata.data?.abi || [],
"ModularCore",
extensions,
),
[publishMetadata.data?.abi, extensions],
);
const hasExtensionsParam = useMemo(
() =>
constructorParams.some(
(param) => param.name === "_extensions" || "_marketplaceV3Params",
),
[constructorParams],
);
const shouldShowDynamicFactoryInput = useMemo(
() =>
isPluginRouter ||
(isDynamicContract && hasExtensionsParam) ||
isModularContract,
[isPluginRouter, isDynamicContract, isModularContract, hasExtensionsParam],
);
const shouldShowModularFactoryInput = useMemo(
() => isModularContract,
[isModularContract],
);
const shouldShowModulesParamInput = useMemo(
() => isModularContract,
[isModularContract],
);
// during loading and after success we should stay in loading state
const isLoading = publishMutation.isPending || publishMutation.isSuccess;
const isLoading = sendTx.isPending || sendTx.isSuccess;
useIsomorphicLayoutEffect(() => {
window?.scrollTo({
@@ -312,6 +188,10 @@ export const ContractPublishForm: React.FC<ContractPublishFormProps> = ({
as="form"
id="contract-release-form"
onSubmit={form.handleSubmit((data) => {
if (!account) {
// no account, do nothing
return;
}
const addressArray =
Object.keys(data?.customFactoryAddresses || {}).length > 0
? (data?.customFactoryAddresses as unknown as {
@@ -332,80 +212,83 @@ export const ContractPublishForm: React.FC<ContractPublishFormProps> = ({
category: "publish",
action: "click",
label: "attempt",
uris: contractId,
release_id: `${ensNameOrAddress}/${publishMetadata.data?.name}`,
release_id: `${ensNameOrAddress}/${props.publishMetadata.name}`,
});
publishMutation.mutate(
{
predeployUri: contractId,
extraMetadata: {
...data,
networksForDeployment: {
allNetworks:
data.deployType === "customFactory"
? false
: data.networksForDeployment?.allNetworks,
networksEnabled:
addressArray.length > 0
? Object.keys(addressObj).map((val) => Number(val))
: data.networksForDeployment?.networksEnabled,
},
factoryDeploymentData: {
...data.factoryDeploymentData,
implementationAddresses:
data.factoryDeploymentData?.implementationAddresses || {},
customFactoryInput: {
factoryFunction:
data.factoryDeploymentData?.customFactoryInput
?.factoryFunction || "deployProxyByImplementation",
customFactoryAddresses:
addressObj ||
data.factoryDeploymentData?.customFactoryInput
?.customFactoryAddresses,
},
modularFactoryInput: {
modulesParamName:
data.factoryDeploymentData?.modularFactoryInput
?.modulesParamName || "",
},
},
},
contractName: publishMetadata.data?.name,
const metadata = {
...data,
networksForDeployment: {
allNetworks:
data.deployType === "customFactory"
? false
: data.networksForDeployment?.allNetworks,
networksEnabled:
addressArray.length > 0
? Object.keys(addressObj).map((val) => Number(val))
: data.networksForDeployment?.networksEnabled,
},
{
onSuccess: () => {
onSuccess();
trackEvent({
category: "publish",
action: "click",
label: "success",
uris: contractId,
release_id: `${ensNameOrAddress}/${publishMetadata.data?.name}`,
version: data.version,
type: data.deployType,
});
if (successRedirectUrl) {
router.push(
successRedirectUrl,
undefined,
// reset scroll after redirect
{ scroll: true },
);
}
},
onError: (err) => {
onError(err);
trackEvent({
category: "publish",
action: "click",
label: "error",
uris: contractId,
release_id: `${ensNameOrAddress}/${publishMetadata.data?.name}`,
is_factory: data.isDeployableViaFactory,
});
factoryDeploymentData: {
...data.factoryDeploymentData,
implementationAddresses:
data.factoryDeploymentData?.implementationAddresses || {},
implementationInitializerFunction:
data.factoryDeploymentData
?.implementationInitializerFunction || "initialize",
customFactoryInput: {
factoryFunction:
data.factoryDeploymentData?.customFactoryInput
?.factoryFunction || "deployProxyByImplementation",
params:
data.factoryDeploymentData?.customFactoryInput?.params ||
[],
customFactoryAddresses:
addressObj ||
data.factoryDeploymentData?.customFactoryInput
?.customFactoryAddresses,
},
},
);
} satisfies FetchDeployMetadataResult & {
name: string;
version: string;
};
const tx = publishContract({
account,
contract: getContractPublisher(thirdwebClient),
metadata,
previousMetadata: props.publishMetadata,
});
sendTx.mutate(tx, {
onSuccess: async () => {
onSuccess();
trackEvent({
category: "publish",
action: "click",
label: "success",
release_id: `${ensNameOrAddress}/${props.publishMetadata.name}`,
version: data.version,
type: data.deployType,
});
await props.onPublishSuccess().catch((err) => {
console.error("Failed to run onPublishSuccess", err);
});
if (successRedirectUrl) {
router.push(successRedirectUrl, { scroll: true });
}
},
onError: (err) => {
console.error("Failed to publish contract", err);
onError(err);
trackEvent({
category: "publish",
action: "click",
label: "error",
release_id: `${ensNameOrAddress}/${props.publishMetadata.name}`,
is_factory: data.isDeployableViaFactory,
});
},
});
})}
direction="column"
gap={6}
@@ -444,12 +327,8 @@ export const ContractPublishForm: React.FC<ContractPublishFormProps> = ({
{fieldsetToShow === "factory" && (
<Flex flexDir="column" gap={24}>
<FactoryFieldset
abi={(publishMetadata.data?.abi || []) as Abi}
abi={props.publishMetadata.abi || []}
setCustomFactoryAbi={setCustomFactoryAbi}
shouldShowDynamicFactoryInput={shouldShowDynamicFactoryInput}
shouldShowModularFactoryInput={shouldShowModularFactoryInput}
shouldShowModulesParamInput={shouldShowModulesParamInput}
deployParams={deployParams}
/>
</Flex>
)}
@@ -466,7 +345,7 @@ export const ContractPublishForm: React.FC<ContractPublishFormProps> = ({
flexDir={{ base: "column", md: "row" }}
gap={4}
>
{!address ? (
{!account ? (
<>
<Box />
<CustomConnectWallet />
@@ -524,7 +403,7 @@ export const ContractPublishForm: React.FC<ContractPublishFormProps> = ({
form="contract-release-form"
isDisabled={isDisabled}
loadingText={
publishMutation.isSuccess
sendTx.isSuccess
? "Preparing page"
: "Publishing contract"
}
@@ -540,4 +419,4 @@ export const ContractPublishForm: React.FC<ContractPublishFormProps> = ({
</Box>
</FormProvider>
);
};
}
@@ -11,7 +11,6 @@ import {
Tabs,
Textarea,
} from "@chakra-ui/react";
import type { ExtraPublishMetadata } from "@thirdweb-dev/sdk";
import { compare, validate } from "compare-versions";
import { FileInput } from "components/shared/FileInput";
import { SolidityInput } from "contract-ui/components/solidity-inputs";
@@ -19,6 +18,7 @@ import { SelectOption } from "core-ui/batch-upload/lazy-mint-form/select-option"
import { useImageFileOrUrl } from "hooks/useImageFileOrUrl";
import { replaceIpfsUrl } from "lib/sdk";
import { useFormContext } from "react-hook-form";
import type { ExtendedMetadata } from "thirdweb/utils";
import {
Card,
FormErrorMessage,
@@ -40,7 +40,7 @@ export const LandingFieldset: React.FC<LandingFieldsetProps> = ({
latestVersion,
placeholderVersion,
}) => {
const form = useFormContext<ExtraPublishMetadata>();
const form = useFormContext<ExtendedMetadata>();
const logoUrl = useImageFileOrUrl(form.watch("logo"));
const handleVersionChange = (e: React.ChangeEvent<HTMLInputElement>) => {
@@ -95,6 +95,7 @@ export const LandingFieldset: React.FC<LandingFieldsetProps> = ({
<FileInput
accept={{ "image/*": [] }}
value={logoUrl}
// @ts-expect-error - we upload the file later this is fine
setValue={(file) => form.setValue("logo", file)}
className="border border-border rounded transition-all duration-200"
renderPreview={(fileUrl) => (
@@ -1,46 +0,0 @@
import { Box, Flex, Icon } from "@chakra-ui/react";
import { useFieldArray, useFormContext } from "react-hook-form";
import { FiPlus } from "react-icons/fi";
import { Button, Heading, Text } from "tw-components";
import { ModuleInput } from "./module-input";
export const ModularContractsFieldset = () => {
const form = useFormContext();
const { fields, append, remove } = useFieldArray({
name: "defaultModules",
control: form.control,
});
return (
<Flex gap={8} direction="column" as="fieldset">
<Flex gap={2} direction="column">
<Heading size="title.md">Contract module settings</Heading>
<Text>You can set default modules for your contract.</Text>
</Flex>
<Flex flexDir="column" gap={4}>
{fields.map((item, index) => (
<ModuleInput key={item.id} remove={remove} index={index} />
))}
<Box>
<Button
type="button"
size="sm"
colorScheme="primary"
borderRadius="md"
leftIcon={<Icon as={FiPlus} />}
onClick={() =>
append({
moduleName: "",
moduleVersion: "",
publisherAddress: "",
})
}
>
Add Module
</Button>
</Box>
</Flex>
</Flex>
);
};
@@ -1,118 +0,0 @@
import {
Divider,
Flex,
FormControl,
Icon,
IconButton,
Input,
Select,
Skeleton,
} from "@chakra-ui/react";
import { useFormContext } from "react-hook-form";
import { FiTrash } from "react-icons/fi";
import { FormLabel } from "tw-components";
import { useAllVersions, usePublishedContractsQuery } from "../hooks";
interface ModuleInputProps {
index: number;
remove: (index: number) => void;
}
export const ModuleInput: React.FC<ModuleInputProps> = ({ index, remove }) => {
const form = useFormContext();
const feature = "ModularModule";
const publishedContractsQuery = usePublishedContractsQuery(
form.watch(`defaultModules.${index}.publisherAddress`),
feature,
);
const allVersions = useAllVersions(
form.watch(`defaultModules.${index}.publisherAddress`),
form.watch(`defaultModules.${index}.moduleName`),
);
return (
<Flex flexDir="column" gap={2}>
<Flex
w="full"
gap={{ base: 4, md: 2 }}
flexDir={{ base: "column", md: "row" }}
>
<FormControl
as={Flex}
flexDir="column"
gap={1}
isInvalid={
!!form.getFieldState(
`defaultModules.${index}.publisherAddress`,
form.formState,
).error
}
>
<FormLabel textTransform="capitalize">Publisher</FormLabel>
<Input
placeholder="Address or ENS"
{...form.register(`defaultModules.${index}.publisherAddress`)}
/>
</FormControl>
<FormControl as={Flex} flexDir="column" gap={1}>
<FormLabel textTransform="capitalize">Module Name</FormLabel>
<Skeleton
isLoaded={
!!publishedContractsQuery.data ||
!publishedContractsQuery.isFetching
}
borderRadius="lg"
>
<Select
isDisabled={(publishedContractsQuery?.data || []).length === 0}
{...form.register(`defaultModules.${index}.moduleName`)}
placeholder={
publishedContractsQuery.isFetched &&
(publishedContractsQuery?.data || []).length === 0
? "No modules found"
: "Select module"
}
>
{publishedContractsQuery?.data?.map(({ id }) => (
<option key={id} value={id}>
{id}
</option>
))}
</Select>
</Skeleton>
</FormControl>
<FormControl as={Flex} flexDir="column" gap={1}>
<FormLabel textTransform="capitalize">Module Version</FormLabel>
<Skeleton
isLoaded={!!allVersions.data || !allVersions.isFetching}
borderRadius="lg"
>
<Select
w="full"
isDisabled={!allVersions.data}
{...form.register(`defaultModules.${index}.moduleVersion`)}
borderRadius="lg"
>
<option value="">Always latest</option>
{allVersions?.data?.map(({ version }) => (
<option key={version} value={version}>
{version}
</option>
))}
</Select>
</Skeleton>
</FormControl>
<IconButton
icon={<Icon as={FiTrash} boxSize={5} />}
aria-label="Remove row"
onClick={() => remove(index)}
alignSelf="end"
/>
</Flex>
<Divider />
</Flex>
);
};
@@ -1,41 +0,0 @@
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import type { AbiParameter } from "abitype";
import type React from "react";
interface ModulesParamSelectorProps {
deployParams: readonly AbiParameter[];
value: string;
onChange: (fn: string) => void;
}
export const ModulesParamSelector: React.FC<ModulesParamSelectorProps> = ({
deployParams,
value,
onChange,
}) => {
return (
<Select value={value} onValueChange={onChange}>
<SelectTrigger>
<SelectValue placeholder="Select Param" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{deployParams.map((param) => {
return (
<SelectItem key={param.name} value={param.name || ""}>
{param.name}
</SelectItem>
);
})}
</SelectGroup>
</SelectContent>
</Select>
);
};
@@ -47,7 +47,7 @@ function convertContractDataToRowData(
input: ContractDataInput,
): ContractDataRow {
return {
id: input.id,
id: input.contractId,
...input.metadata,
};
}
@@ -1,26 +1,25 @@
import { Skeleton } from "@chakra-ui/react";
import { useContractPrePublishMetadata } from "components/contract-components/hooks";
import { useFetchDeployMetadata } from "components/contract-components/hooks";
import type { DeployableContractContractCellProps } from "components/contract-components/types";
import { useActiveAccount } from "thirdweb/react";
import { Text } from "tw-components";
export const ContractDescriptionCell: React.FC<
DeployableContractContractCellProps
> = ({ cell: { value } }) => {
const address = useActiveAccount()?.address;
const fullPublishMetadata = useContractPrePublishMetadata(value, address);
const deployMetadataResultQuery = useFetchDeployMetadata(value);
return (
<Skeleton
isLoaded={
fullPublishMetadata.isSuccess || !fullPublishMetadata.isFetching
deployMetadataResultQuery.isSuccess ||
!deployMetadataResultQuery.isFetching
}
>
<Text size="body.md" noOfLines={1}>
{fullPublishMetadata.data?.latestPublishedContractMetadata
{deployMetadataResultQuery.data?.latestPublishedContractMetadata
?.publishedMetadata.description ||
(!fullPublishMetadata.isFetching ? "First Version" : "None")}
(!deployMetadataResultQuery.isFetching ? "First Version" : "None")}
</Text>
</Skeleton>
);
@@ -1,12 +1,12 @@
import { Skeleton } from "@chakra-ui/react";
import { useContractPublishMetadataFromURI } from "components/contract-components/hooks";
import { useFetchDeployMetadata } from "components/contract-components/hooks";
import type { DeployableContractContractCellProps } from "components/contract-components/types";
import { Text } from "tw-components";
export const ContractNameCell: React.FC<
DeployableContractContractCellProps
> = ({ cell: { value } }) => {
const publishMetadata = useContractPublishMetadataFromURI(value);
const publishMetadata = useFetchDeployMetadata(value);
return (
<Skeleton isLoaded={publishMetadata.isSuccess}>
@@ -1,41 +1,22 @@
import { thirdwebClient } from "@/constants/client";
import { networkKeys, useDashboardEVMChainId } from "@3rdweb-sdk/react";
import { useMutationWithInvalidate } from "@3rdweb-sdk/react/hooks/query/useQueryWithNetwork";
import {
type QueryClient,
queryOptions,
useQuery,
useQueryClient,
} from "@tanstack/react-query";
import {
type ExtraPublishMetadata,
type FeatureName,
type FeatureWithEnabled,
type ThirdwebSDK,
detectFeatures,
fetchContractMetadata,
fetchPreDeployMetadata,
isExtensionEnabled,
} from "@thirdweb-dev/sdk";
import { queryOptions, useQuery } from "@tanstack/react-query";
import type { Abi } from "abitype";
import type { ProfileMetadataInput } from "constants/schemas";
import { useSupportedChain } from "hooks/chains/configureChains";
import { isEnsName, resolveEns } from "lib/ens";
import { getDashboardChainRpc } from "lib/rpc";
import {
StorageSingleton,
getPolygonGaslessSDK,
getThirdwebSDK,
} from "lib/sdk";
import { useMemo } from "react";
import { getContract } from "thirdweb";
import { polygon } from "thirdweb/chains";
import { resolveContractAbi } from "thirdweb/contract";
import { useActiveAccount } from "thirdweb/react";
import { isAddress } from "thirdweb/utils";
import { type ThirdwebContract, getContract } from "thirdweb";
import {
getBytecode,
resolveContractAbi,
fetchDeployMetadata as sdkFetchDeployMetadata,
} from "thirdweb/contract";
import {
getAllPublishedContracts,
getContractPublisher,
getPublishedUriFromCompilerUri,
} from "thirdweb/extensions/thirdweb";
import { extractIPFSUri, isAddress } from "thirdweb/utils";
import invariant from "tiny-invariant";
import { useV5DashboardChain } from "../../lib/v5-adapter";
import { useEthersSigner } from "../app-layouts/provider-setup";
import {
type PublishedContractWithVersion,
fetchPublishedContractVersions,
@@ -43,133 +24,45 @@ import {
} from "./fetch-contracts-with-versions";
import type { ContractId } from "./types";
// biome-ignore lint/suspicious/noExplicitAny: FIXME
function removeUndefinedFromObject(obj: Record<string, any>) {
// biome-ignore lint/suspicious/noExplicitAny: FIXME
const newObj: Record<string, any> = {};
function isAnyObject(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null;
}
function removeUndefinedFromObjectDeep<T extends Record<string, unknown>>(
obj: T,
): T {
const newObj = {} as T;
for (const key in obj) {
if (obj[key] !== undefined) {
newObj[key] = obj[key];
} else if (isAnyObject(obj[key])) {
newObj[key] = removeUndefinedFromObjectDeep(obj[key]);
}
}
return newObj;
}
// metadata PRE publish, only has the compiler output info (from CLI)
async function fetchContractPublishMetadataFromURI(contractId: ContractId) {
async function fetchDeployMetadata(contractId: string) {
const contractIdIpfsHash = toContractIdIpfsHash(contractId);
invariant(contractId !== "ipfs://undefined", "uri can't be undefined");
let resolved: Awaited<ReturnType<typeof fetchPreDeployMetadata>> | undefined =
undefined;
try {
resolved = await fetchPreDeployMetadata(
contractIdIpfsHash,
StorageSingleton,
);
} catch (err) {
console.error("failed to resolvePreDeployMetadata", err);
}
if (!resolved) {
return {
name: "",
image: "custom",
};
}
return {
// biome-ignore lint/suspicious/noExplicitAny: FIXME
image: (resolved as any)?.image || "custom",
name: resolved.name,
description: resolved.info?.title || "",
abi: resolved.abi,
info: removeUndefinedFromObject(resolved.info),
licenses: resolved.licenses,
compilerMetadata: resolved.metadata,
analytics: removeUndefinedFromObject(resolved.analytics),
};
return removeUndefinedFromObjectDeep(
await sdkFetchDeployMetadata({
client: thirdwebClient,
uri: contractIdIpfsHash,
}),
);
}
export function useContractPublishMetadataFromURI(contractId: ContractId) {
export function useFetchDeployMetadata(contractId: ContractId) {
return useQuery({
queryKey: ["publish-metadata", contractId],
queryFn: () => fetchContractPublishMetadataFromURI(contractId),
queryFn: () => fetchDeployMetadata(contractId),
enabled: !!contractId,
});
}
// metadata PRE publish, only contains the compiler output
// if passing an address, also fetches the latest version of the matching contract
export function useContractPrePublishMetadata(uri: string, address?: string) {
const contractIdIpfsHash = toContractIdIpfsHash(uri);
return useQuery({
queryKey: ["pre-publish-metadata", uri, address],
queryFn: async () => {
invariant(address, "address is not defined");
// TODO: Make this nicer.
invariant(uri !== "ipfs://undefined", "uri can't be undefined");
const sdk = getThirdwebSDK(
polygon.id,
getDashboardChainRpc(polygon.id, undefined),
);
return await sdk
?.getPublisher()
.fetchPrePublishMetadata(contractIdIpfsHash, address);
},
enabled: !!uri && !!address,
});
}
async function fetchFullPublishMetadata(
sdk: ThirdwebSDK,
uri: string,
queryClient: QueryClient,
) {
const rawPublishMetadata = await sdk
.getPublisher()
.fetchFullPublishMetadata(uri);
const ensResult = rawPublishMetadata.publisher
? await queryClient.fetchQuery(ensQuery(rawPublishMetadata.publisher))
: undefined;
return {
...rawPublishMetadata,
publisher:
ensResult?.ensName || ensResult?.address || rawPublishMetadata.publisher,
};
}
// Metadata POST publish, contains all the extra information filled in by the user
export function useContractFullPublishMetadata(uri: string) {
const contractIdIpfsHash = toContractIdIpfsHash(uri);
const queryClient = useQueryClient();
return useQuery({
queryKey: ["full-publish-metadata", uri],
queryFn: async () => {
const sdk = getThirdwebSDK(
polygon.id,
getDashboardChainRpc(polygon.id, undefined),
);
// TODO: Make this nicer.
invariant(uri !== "ipfs://undefined", "uri can't be undefined");
return await fetchFullPublishMetadata(
sdk,
contractIdIpfsHash,
queryClient,
);
},
enabled: !!uri,
});
}
export function publisherProfileQuery(publisherAddress?: string) {
function publisherProfileQuery(publisherAddress?: string) {
return queryOptions({
queryKey: ["releaser-profile", publisherAddress],
queryFn: () => {
@@ -208,45 +101,31 @@ export function useAllVersions(
});
}
export function usePublishedContractsFromDeploy(
contractAddress?: string,
chainId?: number,
) {
const activeChainId = useDashboardEVMChainId();
const cId = chainId || activeChainId;
const chainInfo = useSupportedChain(cId || -1);
export function usePublishedContractsFromDeploy(contract: ThirdwebContract) {
return useQuery({
queryKey: (networkKeys.chain(cId) as readonly unknown[]).concat([
"release-from-deploy",
contractAddress,
]),
queryKey: [
"published-contracts-from-deploy",
contract.chain.id,
contract.address,
],
queryFn: async () => {
invariant(contractAddress, "contractAddress is not defined");
invariant(cId, "chain not defined");
const bytecode = await getBytecode(contract);
const contractUri = extractIPFSUri(bytecode);
if (!contractUri) {
throw new Error("No IPFS URI found in bytecode");
}
const rpcUrl = chainInfo
? getDashboardChainRpc(cId, chainInfo)
: undefined;
const publishURIs = await getPublishedUriFromCompilerUri({
contract: getContractPublisher(thirdwebClient),
compilerMetadataUri: contractUri,
});
invariant(rpcUrl, "rpcUrl not defined");
const sdk = getThirdwebSDK(cId, rpcUrl);
const contractUri = await sdk
.getPublisher()
.resolveContractUriFromAddress(contractAddress);
const polygonSdk = getThirdwebSDK(
polygon.id,
getDashboardChainRpc(polygon.id, undefined),
return await Promise.all(
publishURIs.map((uri) => fetchDeployMetadata(uri)),
);
return await polygonSdk
.getPublisher()
.resolvePublishMetadataFromCompilerMetadata(contractUri);
},
enabled: !!contractAddress && !!cId && !!chainInfo,
enabled: !!contract,
retry: false,
});
}
@@ -305,205 +184,34 @@ function toContractIdIpfsHash(contractId: ContractId) {
return `ipfs://${contractId}`;
}
interface PublishMutationData {
predeployUri: string;
extraMetadata: ExtraPublishMetadata;
contractName?: string;
}
export function usePublishMutation() {
const address = useActiveAccount()?.address;
// TODO: remove this obviously
const signer = useEthersSigner();
return useMutationWithInvalidate(
async ({ predeployUri, extraMetadata }: PublishMutationData) => {
if (!signer) {
throw new Error("Signer not found");
}
const sdk = getPolygonGaslessSDK(signer);
invariant(
sdk && "getPublisher" in sdk,
"sdk is not ready or does not support publishing",
);
const contractIdIpfsHash = toContractIdIpfsHash(predeployUri);
await sdk.getPublisher().publish(contractIdIpfsHash, extraMetadata);
},
{
onSuccess: (_data, variables, _options, invalidate) => {
return Promise.all([
invalidate([["pre-publish-metadata", variables.predeployUri]]),
fetch(
`/api/revalidate/publish?address=${address}&contractName=${variables.contractName}`,
).catch((err) => console.error("failed to revalidate", err)),
]);
},
},
);
}
export function useEditProfileMutation() {
const address = useActiveAccount()?.address;
// TODO: remove this obviously
const signer = useEthersSigner();
return useMutationWithInvalidate(
async (data: ProfileMetadataInput) => {
if (!signer) {
throw new Error("Signer not found");
}
const sdk = getPolygonGaslessSDK(signer);
await sdk.getPublisher().updatePublisherProfile(data);
},
{
onSuccess: (_data, _variables, _options, invalidate) => {
return Promise.all([
invalidate([["releaser-profile", address]]),
fetch(`/api/revalidate/publish?address=${address}`).catch((err) =>
console.error("failed to revalidate", err),
),
]);
},
},
);
}
export async function fetchPublishedContracts(
sdk: ThirdwebSDK,
queryClient: QueryClient,
address?: string | null,
) {
invariant(sdk, "sdk not provided");
async function fetchPublishedContracts(address?: string | null) {
invariant(address, "address is not defined");
const tempResult = ((await sdk.getPublisher().getAll(address)) || []).filter(
(c) => c.id,
);
const tempResult = (
(await getAllPublishedContracts({
contract: getContractPublisher(thirdwebClient),
publisher: address,
})) || []
).filter((c) => c.contractId);
return await Promise.all(
tempResult.map(async (c) => ({
...c,
metadata: await fetchFullPublishMetadata(sdk, c.metadataUri, queryClient),
metadata: await fetchDeployMetadata(c.publishMetadataUri),
})),
);
}
async function fetchPublishedContractsWithFeature(
sdk: ThirdwebSDK,
queryClient: QueryClient,
feature: FeatureName,
address?: string | null,
) {
invariant(sdk, "sdk not provided");
invariant(address, "address is not defined");
const tempResult = ((await sdk.getPublisher().getAll(address)) || []).filter(
(c) => c.id,
);
const tempResultWithMetadata = await Promise.all(
tempResult.map(async (c) => ({
...c,
metadata: await fetchFullPublishMetadata(sdk, c.metadataUri, queryClient),
})),
);
const resultWithFeature = await Promise.all(
tempResultWithMetadata.map(async (c) => ({
...c,
deployMetadata: await fetchContractMetadata(
c.metadata.metadataUri,
StorageSingleton,
),
})),
);
return resultWithFeature.filter((c) => {
// @ts-expect-error - this is the "wrong" abi type, but it works fine
const extensions = detectFeatures(c.deployMetadata.abi as Abi);
// @ts-expect-error - this is the "wrong" abi type, but it works fine
return isExtensionEnabled(c.deployMetadata.abi as Abi, feature, extensions);
});
}
export type PublishedContractDetails = Awaited<
ReturnType<typeof fetchPublishedContracts>
>[number];
export function usePublishedContractsQuery(
address?: string,
feature?: FeatureName,
) {
const queryClient = useQueryClient();
export function usePublishedContractsQuery(address?: string) {
return useQuery<PublishedContractDetails[]>({
queryKey: ["published-contracts", address, feature],
queryFn: () => {
const sdk = getThirdwebSDK(
polygon.id,
getDashboardChainRpc(polygon.id, undefined),
);
return feature && feature.length > 0
? fetchPublishedContractsWithFeature(sdk, queryClient, feature, address)
: fetchPublishedContracts(sdk, queryClient, address);
},
queryKey: ["published-contracts", address],
queryFn: () => fetchPublishedContracts(address),
enabled: !!address,
});
}
const ALWAYS_SUGGESTED = ["ContractMetadata", "Permissions"];
function extractExtensions(
input: ReturnType<typeof detectFeatures>,
enabledExtensions: FeatureWithEnabled[] = [],
suggestedExtensions: FeatureWithEnabled[] = [],
parent = "__ROOT__",
) {
if (!input) {
return {
enabledExtensions,
suggestedExtensions,
};
}
for (const extensionKey in input) {
const extension = input[extensionKey];
// if extension is enabled, then add it to enabledFeatures
if (extension.enabled) {
enabledExtensions.push(extension);
}
// otherwise if it is disabled, but it's parent is enabled or suggested, then add it to suggestedFeatures
else if (
enabledExtensions.findIndex((f) => f.name === parent) > -1 ||
ALWAYS_SUGGESTED.includes(extension.name)
) {
suggestedExtensions.push(extension);
}
// recurse
extractExtensions(
extension.features,
enabledExtensions,
suggestedExtensions,
extension.name,
);
}
return {
enabledExtensions,
suggestedExtensions,
};
}
function useContractDetectedExtensions(abi?: Abi) {
const features = useMemo(() => {
if (abi) {
// @ts-expect-error - this is the "wrong" abi type, but it works fine
return extractExtensions(detectFeatures(abi));
}
return undefined;
}, [abi]);
return features;
}
export function useContractEnabledExtensions(abi?: Abi) {
const extensions = useContractDetectedExtensions(abi);
return extensions ? extensions.enabledExtensions : [];
}
export function ensQuery(addressOrEnsName?: string) {
// if the address is `thirdweb.eth` we actually want `deployer.thirdweb.eth` here...
if (addressOrEnsName === "thirdweb.eth") {
@@ -1,3 +1,4 @@
import { thirdwebClient } from "@/constants/client";
import {
Box,
Flex,
@@ -10,7 +11,12 @@ import {
import { SiDiscord } from "@react-icons/all-files/si/SiDiscord";
import { SiGithub } from "@react-icons/all-files/si/SiGithub";
import { SiTwitter } from "@react-icons/all-files/si/SiTwitter";
import { useQueryClient } from "@tanstack/react-query";
import { FileInput } from "components/shared/FileInput";
import {
DASHBOARD_ENGINE_RELAYER_URL,
DASHBOARD_FORWARDER_ADDRESS,
} from "constants/misc";
import type { ProfileMetadata, ProfileMetadataInput } from "constants/schemas";
import { useTrack } from "hooks/analytics/useTrack";
import { useImageFileOrUrl } from "hooks/useImageFileOrUrl";
@@ -20,6 +26,12 @@ import { useForm } from "react-hook-form";
import { BiImage } from "react-icons/bi";
import { FiEdit, FiGlobe } from "react-icons/fi";
import { HiPencilAlt } from "react-icons/hi";
import {
getContractPublisher,
setPublisherProfileUri,
} from "thirdweb/extensions/thirdweb";
import { useActiveAccount, useSendAndConfirmTransaction } from "thirdweb/react";
import { upload } from "thirdweb/storage";
import {
Button,
Drawer,
@@ -28,7 +40,6 @@ import {
Heading,
} from "tw-components";
import { MaskedAvatar } from "tw-components/masked-avatar";
import { useEditProfileMutation } from "../hooks";
interface EditProfileProps {
publisherProfile: ProfileMetadata;
@@ -53,7 +64,16 @@ export const EditProfile: React.FC<EditProfileProps> = ({
const imageUrl = useImageFileOrUrl(watch("avatar"));
const editProfile = useEditProfileMutation();
const address = useActiveAccount()?.address;
const queryClient = useQueryClient();
const sendTx = useSendAndConfirmTransaction({
gasless: {
experimentalChainlessSupport: true,
provider: "engine",
relayerUrl: DASHBOARD_ENGINE_RELAYER_URL,
relayerForwarderAddress: DASHBOARD_FORWARDER_ADDRESS,
},
});
const { onSuccess, onError } = useTxNotifications(
"Profile update successfully",
@@ -76,13 +96,31 @@ export const EditProfile: React.FC<EditProfileProps> = ({
<form
id={FORM_ID}
onSubmit={handleSubmit((d) => {
if (!address) {
return;
}
trackEvent({
category: "profile",
action: "edit",
label: "attempt",
});
editProfile.mutate(d, {
onSuccess: () => {
const tx = setPublisherProfileUri({
contract: getContractPublisher(thirdwebClient),
asyncParams: async () => {
return {
publisher: address,
uri: await upload({
files: [d],
client: thirdwebClient,
}),
};
},
});
sendTx.mutate(tx, {
onSuccess: async () => {
await queryClient.invalidateQueries({
queryKey: ["releaser-profile", address],
});
onSuccess();
trackEvent({
category: "profile",
@@ -120,7 +158,7 @@ export const EditProfile: React.FC<EditProfileProps> = ({
role="group"
colorScheme="primary"
type="submit"
isLoading={editProfile.isPending}
isLoading={sendTx.isPending}
form={FORM_ID}
>
Save
@@ -2,11 +2,7 @@ import { Image, Skeleton } from "@chakra-ui/react";
import { ChakraNextImage, type ChakraNextImageProps } from "components/Image";
import { replaceIpfsUrl } from "lib/sdk";
import type { StaticImageData } from "next/image";
import { useActiveAccount } from "thirdweb/react";
import {
useContractPrePublishMetadata,
useContractPublishMetadataFromURI,
} from "../hooks";
import { useFetchDeployMetadata } from "../hooks";
import type { ContractId } from "../types";
interface ContractIdImageProps
@@ -20,28 +16,22 @@ export const ContractIdImage: React.FC<ContractIdImageProps> = ({
boxSize = 8,
...imgProps
}) => {
const address = useActiveAccount()?.address;
const fullPublishMetadata = useContractPrePublishMetadata(
contractId,
address,
);
const publishMetadata = useContractPublishMetadataFromURI(contractId);
const deployMetadataResultQuery = useFetchDeployMetadata(contractId);
const logo =
fullPublishMetadata.data?.latestPublishedContractMetadata?.publishedMetadata
.logo;
deployMetadataResultQuery.data?.latestPublishedContractMetadata
?.publishedMetadata.logo;
const img =
publishMetadata.data?.image !== "custom"
? publishMetadata.data?.image ||
deployMetadataResultQuery.data?.image !== "custom"
? deployMetadataResultQuery.data?.image ||
require("../../../../public/assets/tw-icons/general.png")
: require("../../../../public/assets/tw-icons/general.png");
const isStaticImage = img && typeof img !== "string";
return (
<Skeleton isLoaded={publishMetadata.isSuccess}>
<Skeleton isLoaded={deployMetadataResultQuery.isSuccess}>
{logo ? (
<Image
alt=""
@@ -54,14 +44,14 @@ export const ContractIdImage: React.FC<ContractIdImageProps> = ({
{...imgProps}
boxSize={boxSize}
src={img as StaticImageData}
alt={publishMetadata.data?.name || "Contract Image"}
alt={deployMetadataResultQuery.data?.name || "Contract Image"}
/>
) : (
<Image
{...imgProps}
boxSize={boxSize}
src={img as string}
alt={publishMetadata.data?.name || "Contract Image"}
alt={deployMetadataResultQuery.data?.name || "Contract Image"}
/>
)}
</Skeleton>
@@ -1,4 +1,3 @@
import { useEVMContractInfo } from "@3rdweb-sdk/react";
import { useQuery } from "@tanstack/react-query";
import {
useEns,
@@ -21,13 +20,8 @@ interface PublishedByProps {
}
export const PublishedBy: React.FC<PublishedByProps> = ({ contract }) => {
const contractEnsQuery = useEns(contract.address);
const activeNetworkInfo = useEVMContractInfo();
const publishedContractsFromDeploy = usePublishedContractsFromDeploy(
contractEnsQuery.data?.address || undefined,
activeNetworkInfo?.chain?.chainId,
);
const publishedContractsFromDeploy =
usePublishedContractsFromDeploy(contract);
const address = useActiveAccount()?.address;
@@ -61,19 +61,16 @@ interface AsyncContractTypeCellProps {
export const AsyncContractTypeCell = memo(
({ cell }: AsyncContractTypeCellProps) => {
const publishedContractsFromDeployQuery = usePublishedContractsFromDeploy(
cell.address,
cell.chainId,
);
const chain = useV5DashboardChain(cell.chainId);
const contract = getContract({
client: thirdwebClient,
address: cell.address,
chain,
});
const publishedContractsFromDeployQuery =
usePublishedContractsFromDeploy(contract);
const contractMetadata = useDashboardContractMetadata(contract);
const contractType =
@@ -1,4 +1,3 @@
import { useResolveContractAbi } from "@3rdweb-sdk/react/hooks/useResolveContractAbi";
import {
Box,
Divider,
@@ -20,20 +19,19 @@ import {
Thead,
Tr,
} from "@chakra-ui/react";
import {
type Abi,
type AbiEvent,
type AbiFunction,
formatAbiItem,
} from "abitype";
import { useContractEnabledExtensions } from "components/contract-components/hooks";
import type { AbiEvent, AbiFunction } from "abitype";
import { camelToTitle } from "contract-ui/components/solidity-inputs/helpers";
import { usePathname, useSearchParams } from "next/navigation";
import { type Dispatch, type SetStateAction, useMemo, useState } from "react";
import type { ThirdwebContract } from "thirdweb";
import * as ERC20Ext from "thirdweb/extensions/erc20";
import * as ERC721Ext from "thirdweb/extensions/erc721";
import * as ERC1155Ext from "thirdweb/extensions/erc1155";
import { useReadContract } from "thirdweb/react";
import { toFunctionSelector } from "thirdweb/utils";
import { Badge, Button, Card, Heading, Text } from "tw-components";
import { useDashboardRouter } from "../../@/lib/DashboardRouter";
import { useContractFunctionSelectors } from "../../contract-ui/hooks/useContractFunctionSelectors";
import {
COMMANDS,
formatSnippet,
@@ -42,72 +40,45 @@ import { CodeSegment } from "../contract-tabs/code/CodeSegment";
import type { CodeEnvironment } from "../contract-tabs/code/types";
import { InteractiveAbiFunction } from "./interactive-abi-function";
// internal helpers
function unique<T>(a: T[], fn: (a: T, b: T) => boolean): T[] {
if (a.length === 0 || a.length === 1) {
return a;
}
if (!fn) {
return a;
}
for (let i = 0; i < a.length; i++) {
for (let j = i + 1; j < a.length; j++) {
if (fn(a[i], a[j])) {
a.splice(j, 1);
j--;
}
}
}
return a;
}
function joinABIs(abis: Abi[], abiWithConstructor?: Abi): Abi {
const filteredABIs: Abi[] = abis.map((abi) =>
abi.filter((item) => item.type !== "constructor"),
);
if (abiWithConstructor) {
filteredABIs.push(abiWithConstructor);
}
const flattenedAbis = filteredABIs.flat();
const finalABIs = unique(flattenedAbis, (a, b): boolean => {
if (a.type !== b.type) {
// cannot be the same if the types are different
return false;
}
// just compare the formatted abi items (since they would be the same if the abi items are the same)
return formatAbiItem(a) === formatAbiItem(b);
});
return finalABIs;
}
interface ContractFunctionProps {
fn?: AbiFunction | AbiEvent;
contract?: ThirdwebContract;
fn: AbiFunction | AbiEvent;
contract: ThirdwebContract;
}
const ContractFunction: React.FC<ContractFunctionProps> = ({
const ContractFunction: React.FC<Partial<ContractFunctionProps>> = ({
fn,
contract,
}) => {
if (!fn || !contract) {
return null;
}
return <ContractFunctionInner contract={contract} fn={fn} />;
};
function ContractFunctionInner({ contract, fn }: ContractFunctionProps) {
const [environment, setEnvironment] = useState<CodeEnvironment>("javascript");
const abiQuery = useResolveContractAbi(contract);
const enabledExtensions = useContractEnabledExtensions(abiQuery.data);
const functionSelectorQuery = useContractFunctionSelectors(contract);
const functionSelectors = functionSelectorQuery.data || [];
const isERC721Query = useReadContract(ERC721Ext.isERC721, { contract });
const isERC1155Query = useReadContract(ERC1155Ext.isERC1155, { contract });
const isERC20 = useMemo(
() => ERC20Ext.isERC20(functionSelectors),
[functionSelectors],
);
const extensionNamespace = useMemo(() => {
if (enabledExtensions.some((e) => e.name === "ERC20")) {
if (isERC20) {
return "erc20";
}
if (enabledExtensions.some((e) => e.name === "ERC721")) {
if (isERC721Query.data) {
return "erc721";
}
if (enabledExtensions.some((e) => e.name === "ERC1155")) {
if (isERC1155Query.data) {
return "erc1155";
}
return undefined;
}, [enabledExtensions]);
}, [isERC20, isERC721Query.data, isERC1155Query.data]);
if (!fn) {
return null;
@@ -138,16 +109,6 @@ const ContractFunction: React.FC<ContractFunctionProps> = ({
</Badge>
)}
</Flex>
{/* TODO: bring this back eventually */}
{/* {fn.comment && (
<MarkdownRenderer
markdownText={fn.comment
?.replaceAll(/See \{(.+)\}(\.)?/gm, "")
.replaceAll("{", '"')
.replaceAll("}", '"')
.replaceAll("'", '"')}
/>
)} */}
{fn.inputs?.length && !contract ? (
<>
<Divider my={2} />
@@ -246,7 +207,7 @@ const ContractFunction: React.FC<ContractFunctionProps> = ({
)}
</Flex>
);
};
}
interface ContractFunctionsPanelProps {
fnsOrEvents: (AbiFunction | AbiEvent)[];
@@ -262,34 +223,16 @@ export const ContractFunctionsPanel: React.FC<ContractFunctionsPanelProps> = ({
fnsOrEvents,
contract,
}) => {
const abiQuery = useResolveContractAbi(contract);
const extensions = useContractEnabledExtensions(abiQuery.data);
// TODO: clean this up
const functionsWithExtension = useMemo(() => {
let allFunctions = fnsOrEvents.filter((f) => f.type === "function");
const allFunctions = fnsOrEvents.filter((f) => f.type === "function");
const results: ExtensionFunctions[] = [];
const processedFunctions: string[] = [];
// biome-ignore lint/complexity/noForEach: FIXME
extensions.forEach((ext) => {
// @ts-expect-error - this is the "wrong" abi type
let functions = joinABIs(ext.abis).filter((f) => f.type === "function");
allFunctions = allFunctions.filter(
(fn) => !functions.map((f) => f.name).includes(fn.name),
);
functions = functions.filter(
(fn) => !processedFunctions.includes(fn.name),
);
processedFunctions.push(...functions.map((fn) => fn.name));
results.push({
extension: ext.name as string,
functions,
});
});
results.push({
extension: "",
functions: allFunctions,
});
return results;
}, [fnsOrEvents, extensions]);
}, [fnsOrEvents]);
const writeFunctions: ExtensionFunctions[] = useMemo(() => {
return functionsWithExtension
.map((e) => {
@@ -16,10 +16,8 @@ export const ContractMetadata: React.FC<ContractMetadataProps> = ({
}) => {
const [wasError, setWasError] = useState(false);
const contractMetadataQuery = useDashboardContractMetadata(contract);
const publishedContractsFromDeploy = usePublishedContractsFromDeploy(
contract.address,
contract.chain.id,
);
const publishedContractsFromDeploy =
usePublishedContractsFromDeploy(contract);
const latestPublished = publishedContractsFromDeploy.data?.slice(-1)[0];
// legitimate, we use this to keep the state around and *only* flip it if the status changes explicitly from error to success etc
@@ -11,13 +11,11 @@ import {
} from "@tanstack/react-query";
import { moduleToBase64 } from "app/(dashboard)/published-contract/utils/module-base-64";
import { ensQuery } from "components/contract-components/hooks";
import { getDashboardChainRpc } from "lib/rpc";
import { getThirdwebSDK } from "lib/sdk";
import { RocketIcon, ShieldCheckIcon } from "lucide-react";
import Link from "next/link";
import { polygon } from "thirdweb/chains";
import { resolveScheme } from "thirdweb/storage";
import invariant from "tiny-invariant";
import { fetchPublishedContractVersion } from "../../contract-components/fetch-contracts-with-versions";
import { ContractPublisher, replaceDeployerAddress } from "../publisher";
interface ContractCardProps {
@@ -262,11 +260,6 @@ async function publishedContractQueryFn(
version: string,
queryClient: QueryClient,
) {
const polygonSdk = getThirdwebSDK(
polygon.id,
getDashboardChainRpc(polygon.id, undefined),
);
const publisherEns = await queryClient.fetchQuery(ensQuery(publisher));
// START prefill both publisher ens variations
if (publisherEns.address) {
@@ -283,17 +276,15 @@ async function publishedContractQueryFn(
}
// END prefill both publisher ens variations
invariant(publisherEns.address, "publisher address not found");
const latestPublishedVersion = await polygonSdk
.getPublisher()
.getVersion(publisherEns.address, contractId, version);
const latestPublishedVersion = await fetchPublishedContractVersion(
publisherEns.address,
contractId,
version,
);
invariant(latestPublishedVersion, "no published version found");
const contractInfo = await polygonSdk
.getPublisher()
.fetchPublishedContractInfo(latestPublishedVersion);
return {
...latestPublishedVersion,
...contractInfo.publishedMetadata,
publishedContractId: `${publisher}/${contractId}/${version}`,
};
}
@@ -1,19 +1,20 @@
import { Button } from "@/components/ui/button";
import { TrackedLinkTW } from "@/components/ui/tracked-link";
import { useLoggedInUser } from "@3rdweb-sdk/react/hooks/useLoggedInUser";
import { useMultiChainRegContractList } from "@3rdweb-sdk/react/hooks/useRegistry";
import { useQuery } from "@tanstack/react-query";
import type { BasicContract } from "contract-ui/types/types";
import { useSupportedChains } from "hooks/chains/configureChains";
import { getDashboardChainRpc } from "lib/rpc";
import { getThirdwebSDK } from "lib/sdk";
import { PlusIcon } from "lucide-react";
import { polygon } from "thirdweb/chains";
import invariant from "tiny-invariant";
import { defineChain, getContract } from "thirdweb";
import { getCompilerMetadata } from "thirdweb/contract";
import { thirdwebClient } from "../../../@/constants/client";
import { FactoryContracts } from "./factory-contracts";
const useFactories = () => {
const { user, isLoggedIn } = useLoggedInUser();
const configuredChains = useSupportedChains();
const contractListQuery = useMultiChainRegContractList(user?.address);
return useQuery({
queryKey: [
"dashboard-registry",
@@ -22,28 +23,20 @@ const useFactories = () => {
"factories",
],
queryFn: async () => {
invariant(user?.address, "user should be logged in");
const polygonSDK = getThirdwebSDK(
polygon.id,
getDashboardChainRpc(polygon.id, undefined),
);
const contractList = await polygonSDK.getMultichainContractList(
user.address,
configuredChains,
);
const contractWithExtensions = await Promise.all(
contractList.map(async (c) => {
const extensions =
"extensions" in c ? await c.extensions().catch(() => []) : [];
return extensions.includes("AccountFactory") ? c : null;
return await Promise.all(
(contractListQuery.data || []).map(async (c) => {
const contract = getContract({
// eslint-disable-next-line no-restricted-syntax
chain: defineChain(c.chainId),
address: c.address,
client: thirdwebClient,
});
const m = await getCompilerMetadata(contract);
return m.name.indexOf("AccountFactory") > -1 ? c : null;
}),
);
return contractWithExtensions.filter((f) => f !== null);
},
enabled: !!user?.address && isLoggedIn,
enabled: !!user?.address && isLoggedIn && !!contractListQuery.data?.length,
});
};
@@ -1,3 +1,4 @@
import { DASHBOARD_STORAGE_URL } from "@/constants/env";
import { useLoggedInUser } from "@3rdweb-sdk/react/hooks/useLoggedInUser";
import { Center, Flex, Tooltip } from "@chakra-ui/react";
import {
@@ -9,7 +10,6 @@ import {
import { createColumnHelper } from "@tanstack/react-table";
import { TWQueryTable } from "components/shared/TWQueryTable";
import { formatDistance } from "date-fns/formatDistance";
import { DASHBOARD_STORAGE_URL } from "lib/sdk";
import { useCallback, useState } from "react";
import { Button, Card, Heading, Text, TrackedCopyButton } from "tw-components";
import { toSize } from "utils/number";
-54
View File
@@ -1,24 +1,3 @@
import type { Chain } from "thirdweb";
import {
arbitrum,
arbitrumSepolia,
avalanche,
avalancheFuji,
base,
baseSepolia,
bsc,
bscTestnet,
ethereum,
fantom,
fantomTestnet,
type hardhat,
localhost,
optimism,
optimismSepolia,
polygon,
sepolia,
} from "thirdweb/chains";
// TODO: move to API
export const OPSponsoredChains = [
// Optimism
@@ -42,36 +21,3 @@ export const OPSponsoredChains = [
//Mantle
5000,
];
export const defaultChains: Chain[] = [
ethereum,
sepolia,
base,
baseSepolia,
polygon,
arbitrum,
arbitrumSepolia,
optimism,
optimismSepolia,
bsc,
bscTestnet,
fantom,
fantomTestnet,
avalanche,
avalancheFuji,
localhost,
];
export type SUPPORTED_CHAIN_ID =
| typeof ethereum.id
| typeof polygon.id
| typeof fantom.id
| typeof fantomTestnet.id
| typeof avalanche.id
| typeof avalancheFuji.id
| typeof optimism.id
| typeof arbitrum.id
| typeof bsc.id
| typeof bscTestnet.id
| typeof hardhat.id
| typeof localhost.id;
@@ -1,7 +1,3 @@
import type {
ClaimCondition as LegacyClaimCondition,
ClaimConditionInputSchema as LegacyClaimConditionInputSchema,
} from "@thirdweb-dev/sdk";
import {
type BaseTransactionOptions,
type ThirdwebClient,
@@ -16,6 +12,10 @@ import * as ERC1155Ext from "thirdweb/extensions/erc1155";
import { download } from "thirdweb/storage";
import { maxUint256 } from "thirdweb/utils";
import type { z } from "zod";
import type {
ClaimCondition as LegacyClaimCondition,
ClaimConditionInputSchema as LegacyClaimConditionInputSchema,
} from "../../legacy-zod-schema";
type SnapshotEntry = {
address: string;
@@ -13,11 +13,6 @@ import {
MenuList,
Spinner,
} from "@chakra-ui/react";
import {
type ClaimConditionInput,
ClaimConditionInputSchema,
type SnapshotEntry,
} from "@thirdweb-dev/sdk";
import { TransactionButton } from "components/buttons/TransactionButton";
import { TooltipBox } from "components/configure-networks/Form/TooltipBox";
import { SnapshotUpload } from "contract-ui/tabs/claim-conditions/components/snapshot-upload";
@@ -46,6 +41,11 @@ import invariant from "tiny-invariant";
import { Button, Heading, MenuItem, Text } from "tw-components";
import * as z from "zod";
import { ZodError } from "zod";
import {
type ClaimConditionInput,
ClaimConditionInputSchema,
type SnapshotEntry,
} from "../../legacy-zod-schema";
import { ResetClaimEligibility } from "../reset-claim-eligibility";
import { getClaimPhasesInLegacyFormat, setClaimPhasesTx } from "./hooks";
import { ClaimConditionsPhase } from "./phase";
@@ -226,6 +226,7 @@ export const ClaimConditionsForm: React.FC<ClaimConditionsFormProps> = ({
return (claimConditionsQuery.data || [])
.map((phase, idx) => ({
...phase,
merkleRootHash: (phase.merkleRootHash || "") as string,
price: phase.currencyMetadata.displayValue,
maxClaimableSupply: phase.maxClaimableSupply?.toString() || "0",
currencyMetadata: {
@@ -0,0 +1,119 @@
import { NATIVE_TOKEN_ADDRESS, ZERO_ADDRESS, toHex } from "thirdweb";
import { z } from "zod";
const RawDateSchema = /* @__PURE__ */ (() =>
z.date().transform((i) => {
return Math.floor(i.getTime() / 1000);
}))();
const StartDateSchema = /* @__PURE__ */ (() =>
RawDateSchema.default(new Date(0)))();
const AmountSchema = /* @__PURE__ */ (() =>
z
.union([
z.string().regex(/^([0-9]+\.?[0-9]*|\.[0-9]+)$/, "Invalid amount"),
z.number().min(0, "Amount cannot be negative"),
])
.transform((arg) => (typeof arg === "number" ? arg.toString() : arg)))();
const QuantitySchema = /* @__PURE__ */ (() =>
z.union([AmountSchema, z.literal("unlimited")]).default("unlimited"))();
const ClaimConditionMetadataSchema = /* @__PURE__ */ (() =>
z
.object({
name: z.string().optional(),
})
.catchall(z.unknown()))();
const BigNumberSchema = /* @__PURE__ */ (() =>
z.union([z.string(), z.number(), z.bigint()]).transform((arg) => {
return BigInt(arg);
}))();
const BigNumberishSchema = /* @__PURE__ */ (() =>
BigNumberSchema.transform((arg) => arg.toString()))();
const SnapshotEntryInput = /* @__PURE__ */ (() =>
z.object({
address: z.string(),
maxClaimable: QuantitySchema.default(0), // defaults to 0
price: QuantitySchema.optional(), // defaults to unlimited, but can be undefined in old snapshots
currencyAddress: z.string().default(ZERO_ADDRESS).optional(), // defaults to AddressZero, but can be undefined for old snapshots
}))();
const SnapshotInputSchema = /* @__PURE__ */ (() =>
z.union([
z.array(z.string()).transform(
async (strings) =>
await Promise.all(
strings.map((address) =>
SnapshotEntryInput.parseAsync({
address,
}),
),
),
),
z.array(SnapshotEntryInput),
]))();
export const ClaimConditionInputSchema = /* @__PURE__ */ (() =>
z.object({
startTime: StartDateSchema,
currencyAddress: z.string().default(NATIVE_TOKEN_ADDRESS),
price: AmountSchema.default(0),
maxClaimableSupply: QuantitySchema,
maxClaimablePerWallet: QuantitySchema,
waitInSeconds: BigNumberishSchema.default(0),
merkleRootHash: z.string().default(toHex("", { size: 32 })),
snapshot: z.optional(SnapshotInputSchema).nullable(),
metadata: ClaimConditionMetadataSchema.optional(),
}))();
const PartialClaimConditionInputSchema = /* @__PURE__ */ (() =>
ClaimConditionInputSchema.partial())();
/**
* @internal
*/
const CurrencySchema = /* @__PURE__ */ (() =>
z.object({
name: z.string(),
symbol: z.string(),
decimals: z.number(),
}))();
/**
* @internal
*/
const CurrencyValueSchema = /* @__PURE__ */ (() =>
CurrencySchema.extend({
value: BigNumberSchema,
displayValue: z.string(),
}))();
const ClaimConditionOutputSchema = /* @__PURE__ */ (() =>
ClaimConditionInputSchema.extend({
availableSupply: QuantitySchema,
currentMintSupply: QuantitySchema,
currencyMetadata: CurrencyValueSchema.default({
value: 0n,
displayValue: "0",
symbol: "",
decimals: 18,
name: "",
}),
price: BigNumberSchema,
waitInSeconds: BigNumberSchema,
startTime: BigNumberSchema.transform((n) => new Date(Number(n) * 1000)),
snapshot: SnapshotInputSchema.optional().nullable(),
}))();
export type SnapshotEntry = z.output<typeof SnapshotEntryInput>;
export type ClaimConditionInput = z.input<
typeof PartialClaimConditionInputSchema
>;
export type ClaimCondition = z.output<typeof ClaimConditionOutputSchema>;
@@ -23,7 +23,6 @@ import {
formatAbiItem,
} from "abitype";
import {
useContractEnabledExtensions,
useContractEvents,
useContractFunctions,
} from "components/contract-components/hooks";
@@ -32,7 +31,12 @@ import type { CodeEnvironment } from "components/contract-tabs/code/types";
import { useSupportedChain } from "hooks/chains/configureChains";
import { useSearchParams } from "next/navigation";
import { useMemo, useState } from "react";
import * as ERC20Ext from "thirdweb/extensions/erc20";
import * as ERC721Ext from "thirdweb/extensions/erc721";
import * as ERC1155Ext from "thirdweb/extensions/erc1155";
import * as ERC4337Ext from "thirdweb/extensions/erc4337";
import { useActiveAccount } from "thirdweb/react";
import { toFunctionSelector } from "thirdweb/utils";
import { Button, Card, Heading, Link, Text, TrackedLink } from "tw-components";
interface CodeOverviewProps {
@@ -542,26 +546,50 @@ export const CodeOverview: React.FC<CodeOverviewProps> = ({
const [tab, setTab] = useState("write");
const enabledExtensions = useContractEnabledExtensions(abi);
const address = useActiveAccount()?.address;
const isMobile = useBreakpointValue({ base: true, md: false });
const isAccountFactory = enabledExtensions.some(
(extension) => extension.name === "AccountFactory",
const functionSelectors = useMemo(() => {
return (abi || [])
.filter((a) => a.type === "function")
.map((fn) => toFunctionSelector(fn));
}, [abi]);
const isAccountFactory = useMemo(() => {
return [
ERC4337Ext.isGetAllAccountsSupported(functionSelectors),
ERC4337Ext.isGetAccountsSupported(functionSelectors),
ERC4337Ext.isTotalAccountsSupported(functionSelectors),
ERC4337Ext.isGetAccountsOfSignerSupported(functionSelectors),
ERC4337Ext.isPredictAccountAddressSupported(functionSelectors),
].every(Boolean);
}, [functionSelectors]);
const isERC20 = useMemo(
() => ERC20Ext.isERC20(functionSelectors),
[functionSelectors],
);
const isERC721 = useMemo(() => {
// this will have to do for now
return [ERC721Ext.isGetNFTsSupported(functionSelectors)].every(Boolean);
}, [functionSelectors]);
const isERC1155 = useMemo(() => {
// this will have to do for now
return [ERC1155Ext.isGetNFTsSupported(functionSelectors)].every(Boolean);
}, [functionSelectors]);
const extensionNamespace = useMemo(() => {
if (enabledExtensions.some((e) => e.name === "ERC20")) {
if (isERC20) {
return "erc20";
}
if (enabledExtensions.some((e) => e.name === "ERC721")) {
if (isERC721) {
return "erc721";
}
if (enabledExtensions.some((e) => e.name === "ERC1155")) {
if (isERC1155) {
return "erc1155";
}
return undefined;
}, [enabledExtensions]);
}, [isERC20, isERC721, isERC1155]);
const chainId = useDashboardEVMChainId() || chainIdProp || 1;
const chainInfo = useSupportedChain(chainId || -1);
@@ -146,7 +146,7 @@ function ModuleRow(props: {
const { contract, moduleAddress, ownerAccount } = props;
const [isUninstallModalOpen, setIsUninstallModalOpen] = useState(false);
const contractInfo = useModuleContractInfo(props.moduleAddress);
const contractInfo = useModuleContractInfo(contract);
const uninstallMutation = useMutation({
mutationFn: async (account: Account) => {
@@ -2,22 +2,32 @@ import { Spinner } from "@/components/ui/Spinner/Spinner";
import { thirdwebClient } from "@/constants/client";
import { FormControl, Input, Select, Skeleton, Spacer } from "@chakra-ui/react";
import { useMutation, useQuery } from "@tanstack/react-query";
import { compatibleModules } from "@thirdweb-dev/sdk";
import { TransactionButton } from "components/buttons/TransactionButton";
import {
useAllVersions,
usePublishedContractsQuery,
} from "components/contract-components/hooks";
import { useMemo } from "react";
import { FormProvider, type UseFormReturn, useForm } from "react-hook-form";
import { toast } from "sonner";
import {
type Chain,
type ContractOptions,
sendTransaction,
waitForReceipt,
} from "thirdweb";
import { installPublishedModule } from "thirdweb/modules";
import {
checkModulesCompatibility,
installPublishedModule,
isGetModuleConfigSupported,
} from "thirdweb/modules";
import { download } from "thirdweb/storage";
import { encodeAbiParameters, resolveImplementation } from "thirdweb/utils";
import {
encodeAbiParameters,
resolveImplementation,
toFunctionSelector,
} from "thirdweb/utils";
import type { Account } from "thirdweb/wallets";
import { FormErrorMessage, FormLabel } from "tw-components";
import {
@@ -55,10 +65,22 @@ export const InstallModuleForm = (props: InstallModuleFormProps) => {
const { contract, account } = props;
const { errors } = formState;
const publishedContractsQuery = usePublishedContractsQuery(
const { data, isLoading, isFetching } = usePublishedContractsQuery(
watch("publisherAddress"),
"ModularModule",
);
// filter out all the contracts that AREN'T modules
const modulesOnly = useMemo(() => {
return (
data?.filter((contract) => {
const fnSelectors = contract.metadata.abi
.filter((fn) => fn.type === "function")
.map((fn) => toFunctionSelector(fn));
return isGetModuleConfigSupported(fnSelectors);
}) || []
);
}, [data]);
const allVersions = useAllVersions(
watch("publisherAddress"),
watch("moduleContract"),
@@ -120,8 +142,8 @@ export const InstallModuleForm = (props: InstallModuleFormProps) => {
required: "Module name is required",
});
const selectedModule = publishedContractsQuery.data?.find(
(x) => x.id === watch("moduleContract"),
const selectedModule = modulesOnly?.find(
(x) => x.contractId === watch("moduleContract"),
);
// Get core contract bytecode
@@ -195,7 +217,7 @@ export const InstallModuleForm = (props: InstallModuleFormProps) => {
contractInfo: {
bytecode: coreContractByteCodeQuery.data,
installedModuleBytecodes: installedModuleBytecodesQuery.data,
chainId: contract.chain.id,
chain: contract.chain,
},
moduleInfo: {
bytecodeUri: selectedModule.metadata.bytecodeUri,
@@ -257,17 +279,14 @@ export const InstallModuleForm = (props: InstallModuleFormProps) => {
>
<FormLabel>Module Name</FormLabel>
<Skeleton
isLoaded={
!!publishedContractsQuery.data ||
!publishedContractsQuery.isFetching
}
isLoaded={!!modulesOnly.length || !isFetching}
borderRadius="lg"
>
<Select
disabled={
installMutation.isPending ||
publishedContractsQuery?.data?.length === 0 ||
publishedContractsQuery.isLoading
modulesOnly?.length === 0 ||
isLoading
}
bg="backgroundHighlight"
{...moduleContractInputProps}
@@ -277,14 +296,12 @@ export const InstallModuleForm = (props: InstallModuleFormProps) => {
moduleContractInputProps.onChange(e);
}}
placeholder={
publishedContractsQuery.data?.length === 0
? "No modules"
: "Select module"
modulesOnly.length === 0 ? "No modules" : "Select module"
}
>
{publishedContractsQuery?.data?.map(({ id }) => (
<option key={id} value={id}>
{id}
{modulesOnly.map(({ contractId }) => (
<option key={contractId} value={contractId}>
{contractId}
</option>
))}
</Select>
@@ -383,7 +400,7 @@ async function isModuleCompatible(options: {
contractInfo: {
bytecode: string;
installedModuleBytecodes: string[];
chainId: number;
chain: Chain;
};
moduleInfo: {
bytecodeUri: string;
@@ -399,11 +416,15 @@ async function isModuleCompatible(options: {
// 2. check compatibility with core and installed modules
try {
const isCompatible = await compatibleModules(
options.contractInfo.bytecode,
[...options.contractInfo.installedModuleBytecodes, moduleBytecode],
options.contractInfo.chainId,
);
const isCompatible = await checkModulesCompatibility({
chain: options.contractInfo.chain,
coreBytecode: options.contractInfo.bytecode,
client: thirdwebClient,
moduleBytecodes: [
moduleBytecode,
...options.contractInfo.installedModuleBytecodes,
],
});
return isCompatible;
} catch (e) {
@@ -1,17 +1,14 @@
import { useEVMContractInfo } from "@3rdweb-sdk/react";
import { usePublishedContractsFromDeploy } from "components/contract-components/hooks";
import { THIRDWEB_DEPLOYER_ADDRESS } from "constants/addresses";
import { useMemo } from "react";
import type { ThirdwebContract } from "thirdweb";
import { useActiveAccount } from "thirdweb/react";
export function useModuleContractInfo(contractAddress: string) {
export function useModuleContractInfo(contract: ThirdwebContract) {
const address = useActiveAccount()?.address;
const activeNetworkInfo = useEVMContractInfo();
const publishedContractsFromDeploy = usePublishedContractsFromDeploy(
contractAddress,
activeNetworkInfo?.chain?.chainId,
);
const publishedContractsFromDeploy =
usePublishedContractsFromDeploy(contract);
const publishedContractToShow = useMemo(() => {
const reversedPublishedContractsFromDeploy = [
@@ -72,12 +72,6 @@ export const ContractNFTPage: React.FC<NftOverviewPageProps> = ({
const canCreateDelayedRevealBatch =
ERC721Ext.isCreateDelayedRevealBatchSupported(functionSelectorQuery.data);
console.log("***DEBUG*** canRenderNFTTable: ", {
isErc721,
canRenderNFTTable,
functionSelectorQuery,
});
return (
<div className="flex flex-col gap-6">
<div className="flex flex-row justify-between items-center">
@@ -1,9 +1,8 @@
import type { SUPPORTED_CHAIN_ID } from "constants/chains";
import type { SolidityType } from "lib/solidity-types";
interface ReplacementProps {
connectedWallet?: string;
chainId?: SUPPORTED_CHAIN_ID;
chainId?: number;
}
interface TemplateValue {
+1 -1
View File
@@ -3,7 +3,7 @@ import { isAddress } from "thirdweb";
import { resolveAddress, resolveName } from "thirdweb/extensions/ens";
import invariant from "tiny-invariant";
export interface ENSResolveResult {
interface ENSResolveResult {
ensName: string | null;
address: string | null;
}
-96
View File
@@ -1,13 +1,3 @@
import {
DASHBOARD_THIRDWEB_CLIENT_ID,
DASHBOARD_THIRDWEB_SECRET_KEY,
IPFS_GATEWAY_URL,
isProd,
} from "@/constants/env";
import { ThirdwebSDK } from "@thirdweb-dev/sdk";
import { ThirdwebStorage } from "@thirdweb-dev/storage";
import type { Signer } from "ethers";
import { polygon } from "thirdweb/chains";
import { resolveScheme } from "thirdweb/storage";
import { thirdwebClient } from "../@/constants/client";
@@ -22,89 +12,3 @@ export function replaceIpfsUrl(uri: string) {
return uri;
}
}
export const DASHBOARD_STORAGE_URL =
process.env.NEXT_PUBLIC_DASHBOARD_UPLOAD_SERVER ||
"https://storage.thirdweb.com";
export const StorageSingleton = new ThirdwebStorage({
gatewayUrls: [IPFS_GATEWAY_URL],
clientId: DASHBOARD_THIRDWEB_CLIENT_ID,
secretKey: DASHBOARD_THIRDWEB_SECRET_KEY,
uploadServerUrl: DASHBOARD_STORAGE_URL,
}) as ThirdwebStorage;
// EVM SDK
const EVM_SDK_MAP = new Map<string, ThirdwebSDK>();
export function getThirdwebSDK(chainId: number, rpcUrl: string): ThirdwebSDK {
try {
new URL(rpcUrl);
} catch (e) {
console.error("Invalid rpcUrl", e, rpcUrl);
// overwrite the rpcUrl with a valid one!
if (isProd) {
// biome-ignore lint/style/noParameterAssign: FIXME
rpcUrl = `https://${chainId}.rpc.thirdweb.com/${DASHBOARD_THIRDWEB_CLIENT_ID}`;
} else {
// biome-ignore lint/style/noParameterAssign: FIXME
rpcUrl = `https://${chainId}.rpc.thirdweb-dev.com/${DASHBOARD_THIRDWEB_CLIENT_ID}`;
}
}
const readonlySettings =
chainId && rpcUrl
? {
chainId,
rpcUrl,
}
: undefined;
// PERF ISSUE - if the sdkOptions is a huge object, stringify will be slow
const sdkKey = chainId + rpcUrl;
let sdk: ThirdwebSDK | null = null;
if (EVM_SDK_MAP.has(sdkKey)) {
sdk = EVM_SDK_MAP.get(sdkKey) as ThirdwebSDK;
} else {
sdk = new ThirdwebSDK(
rpcUrl,
{
readonlySettings,
clientId: DASHBOARD_THIRDWEB_CLIENT_ID,
secretKey: DASHBOARD_THIRDWEB_SECRET_KEY,
},
StorageSingleton,
);
EVM_SDK_MAP.set(sdkKey, sdk);
}
return sdk;
}
export function getPolygonGaslessSDK(signer: Signer) {
let polygonRpcUrl = isProd
? "https://137.rpc.thirdweb.com"
: "https://137.rpc.thirdweb-dev.com";
if (DASHBOARD_THIRDWEB_CLIENT_ID) {
polygonRpcUrl += `/${DASHBOARD_THIRDWEB_CLIENT_ID}`;
}
return ThirdwebSDK.fromSigner(signer, polygonRpcUrl, {
readonlySettings: {
chainId: polygon.id,
rpcUrl: polygonRpcUrl,
},
clientId: DASHBOARD_THIRDWEB_CLIENT_ID,
secretKey: DASHBOARD_THIRDWEB_SECRET_KEY,
gasless: {
engine: {
relayerUrl:
"https://checkout.engine.thirdweb.com/relayer/0c2bdd3a-307f-4243-b6e5-5ba495222d2b",
relayerForwarderAddress: "0x409d530a6961297ece29121dbee2c917c3398659",
},
experimentalChainlessSupport: true,
},
});
}
-3
View File
@@ -167,9 +167,6 @@ export enum PageId {
// thirdweb.com/contracts/publish
PublishMultiple = "publish-multiple-contracts",
// thirdweb.com/contracts/publish/:id
PublishSingle = "publish-single-contract",
// thirdweb.com/:wallet
// example: thirdweb.com/jns.eth
Profile = "profile",
@@ -42,6 +42,7 @@ import { getContractMetadata } from "thirdweb/extensions/common";
import { isERC20 } from "thirdweb/extensions/erc20";
import { isERC721 } from "thirdweb/extensions/erc721";
import { isERC1155 } from "thirdweb/extensions/erc1155";
import { stringify } from "thirdweb/utils";
import { fetchChain } from "utils/fetchChain";
import type { ThirdwebNextPage } from "utils/types";
import { shortenIfAddress } from "utils/usedapp-external";
@@ -454,7 +455,9 @@ export const getStaticProps: GetStaticProps<EVMContractProps> = async (ctx) => {
return {
props: {
dehydratedState: dehydrate(queryClient),
dehydratedState: dehydrate(queryClient, {
serializeData: (d) => stringify(d),
}),
contractInfo: {
chainSlug,
contractAddress: checksummedAddress,
@@ -1,12 +1,11 @@
import { thirdwebClient } from "@/constants/client";
import type { SUPPORTED_CHAIN_ID } from "constants/chains";
import { defineDashboardChain } from "lib/defineDashboardChain";
import type { NextApiRequest, NextApiResponse } from "next";
import { ZERO_ADDRESS, isAddress, toTokens } from "thirdweb";
import { getWalletBalance } from "thirdweb/wallets";
export type BalanceQueryRequest = {
chainId: SUPPORTED_CHAIN_ID;
chainId: number;
address: string;
};
@@ -1,76 +0,0 @@
import { QueryClient } from "@tanstack/react-query";
import {
ensQuery,
fetchPublishedContracts,
} from "components/contract-components/hooks";
import type { ENSResolveResult } from "lib/ens";
import { getDashboardChainRpc } from "lib/rpc";
import { getThirdwebSDK } from "lib/sdk";
import type { NextApiRequest, NextApiResponse } from "next";
import { polygon } from "thirdweb/chains";
import { getSingleQueryValue } from "utils/router";
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method !== "GET") {
return res.status(405).json({ message: "Method not allowed" });
}
const ensNameOrAddress = getSingleQueryValue(req.query, "address");
const contractName = getSingleQueryValue(req.query, "contractName");
if (ensNameOrAddress === undefined) {
return res.status(400).json({ message: "address must be provided" });
}
const queryClient = new QueryClient();
let ensResult: ENSResolveResult;
try {
ensResult = await queryClient.fetchQuery(ensQuery(ensNameOrAddress));
} catch (err) {
return res.status(500).json({ message: (err as Error).message });
}
const pathsToRevalidate: string[] = [];
if (ensResult.address) {
pathsToRevalidate.push(`/${ensResult.address}`);
if (contractName) {
pathsToRevalidate.push(`/${ensResult.address}/${contractName}`);
}
}
if (ensResult.ensName) {
pathsToRevalidate.push(`/${ensResult.ensName}`);
if (contractName) {
pathsToRevalidate.push(`/${ensResult.ensName}/${contractName}`);
}
}
// if we don't have a specific contractName we have to actually fetch all the contracts for the address
if (!contractName) {
const polygonSdk = getThirdwebSDK(
polygon.id,
getDashboardChainRpc(polygon.id, undefined),
);
const publishedContracts = await fetchPublishedContracts(
polygonSdk,
queryClient,
ensResult.address,
);
publishedContracts.map((contract) => {
if (ensResult.address) {
pathsToRevalidate.push(`/${ensResult.address}/${contract.id}`);
}
if (ensResult.ensName) {
pathsToRevalidate.push(`/${ensResult.address}/${contract.id}`);
}
});
}
try {
await Promise.all(pathsToRevalidate.map((path) => res.revalidate(path)));
return res.json({ revalidated: true });
} catch (err) {
console.error("revalidation failed", err);
return res.status(500).send("Error revalidating");
}
};
export default handler;
@@ -1,15 +0,0 @@
// handle old contract paths - redirect to /explore
import type { GetServerSideProps } from "next";
export default function page() {
return null;
}
export const getServerSideProps: GetServerSideProps = async () => {
return {
redirect: {
destination: "/explore",
permanent: false,
},
};
};
@@ -1,25 +0,0 @@
import { Flex } from "@chakra-ui/react";
import { AppLayout } from "components/app-layouts/app";
import { ContractPublishForm } from "components/contract-components/contract-publish-form";
import { useRouter } from "next/router";
import { PageId } from "page-id";
import type { ThirdwebNextPage } from "utils/types";
const ContractsPublishPage: ThirdwebNextPage = () => {
const { query } = useRouter();
const contractId = query.contractId as string | undefined;
return (
<Flex gap={8} direction="column">
{contractId && <ContractPublishForm contractId={contractId} />}
</Flex>
);
};
ContractsPublishPage.getLayout = (page, props) => (
<AppLayout {...props}>{page}</AppLayout>
);
ContractsPublishPage.pageId = PageId.PublishSingle;
export default ContractsPublishPage;
@@ -24,6 +24,7 @@ import type {
import { NextSeo } from "next-seo";
import { PageId } from "page-id";
import { FiChevronLeft } from "react-icons/fi";
import { stringify } from "thirdweb/utils";
import { Heading, Link, Text } from "tw-components";
import { getSingleQueryValue } from "utils/router";
import type { ThirdwebNextPage } from "utils/types";
@@ -165,7 +166,12 @@ export const getStaticProps: GetStaticProps<ExplorePageProps> = async (ctx) => {
await prefetchCategory(category, queryClient);
return {
props: { category, dehydratedState: dehydrate(queryClient) },
props: {
category,
dehydratedState: dehydrate(queryClient, {
serializeData: (d) => stringify(d),
}),
},
};
};
+7 -1
View File
@@ -12,6 +12,7 @@ import type { GetStaticProps, InferGetStaticPropsType } from "next";
import { NextSeo } from "next-seo";
import { PageId } from "page-id";
import { Fragment } from "react";
import { stringify } from "thirdweb/utils";
import type { ThirdwebNextPage } from "utils/types";
import { ContractsSidebarLayout } from "../../core-ui/sidebar/contracts";
@@ -99,6 +100,11 @@ export const getStaticProps: GetStaticProps<ExplorePageProps> = async () => {
await Promise.all(categories.map((c) => prefetchCategory(c, queryClient)));
return {
props: { categories, dehydratedState: dehydrate(queryClient) },
props: {
categories,
dehydratedState: dehydrate(queryClient, {
serializeData: (d) => stringify(d),
}),
},
};
};
@@ -1,15 +1,7 @@
import { useAllContractList } from "@3rdweb-sdk/react/hooks/useRegistry";
import { Box, Flex, Spinner } from "@chakra-ui/react";
import {
type DehydratedState,
QueryClient,
dehydrate,
} from "@tanstack/react-query";
import { AppLayout } from "components/app-layouts/app";
import {
ensQuery,
fetchPublishedContracts,
publisherProfileQuery,
useEns,
usePublishedContractsQuery,
usePublisherProfile,
@@ -20,15 +12,12 @@ import { PublisherAvatar } from "components/contract-components/publisher/masked
import { DeployedContracts } from "components/contract-components/tables/deployed-contracts";
import { PublishedContracts } from "components/contract-components/tables/published-contracts";
import { THIRDWEB_DOMAIN } from "constants/urls";
import { getDashboardChainRpc } from "lib/rpc";
import { getThirdwebSDK } from "lib/sdk";
import type { GetStaticPaths, GetStaticProps } from "next";
import type { GetStaticProps } from "next";
import { NextSeo } from "next-seo";
import { useRouter } from "next/router";
import { ProfileOG } from "og-lib/url-utils";
import { PageId } from "page-id";
import { useEffect, useMemo } from "react";
import { polygon } from "thirdweb/chains";
import { useActiveAccount } from "thirdweb/react";
import { getAddress, isAddress } from "thirdweb/utils";
import { Heading, Text } from "tw-components";
@@ -38,7 +27,6 @@ import { shortenIfAddress } from "utils/usedapp-external";
type UserPageProps = {
profileAddress: string;
dehydratedState: DehydratedState;
};
const UserPage: ThirdwebNextPage = (props: UserPageProps) => {
@@ -218,14 +206,8 @@ UserPage.fallback = (
UserPage.pageId = PageId.Profile;
export default UserPage;
export const getStaticProps: GetStaticProps<UserPageProps> = async (ctx) => {
const queryClient = new QueryClient();
const polygonSdk = getThirdwebSDK(
polygon.id,
getDashboardChainRpc(polygon.id, undefined),
);
const profileAddress = getSingleQueryValue(
// biome-ignore lint/suspicious/noExplicitAny: FIXME
ctx.params as any,
@@ -246,53 +228,14 @@ export const getStaticProps: GetStaticProps<UserPageProps> = async (ctx) => {
? getAddress(lowercaseAddress)
: lowercaseAddress;
let address: string | null;
let ensName: string | null;
try {
const info = await queryClient.fetchQuery(ensQuery(checksummedAddress));
address = info.address;
ensName = info.ensName;
} catch {
// if profileAddress is not a valid address, ensQuery throws
// in that case - redirect to 404
return {
notFound: true,
};
}
if (!address) {
return {
redirect: {
destination: "/explore",
permanent: false,
},
props: {},
};
}
const ensQueries = [queryClient.prefetchQuery(ensQuery(checksummedAddress))];
if (ensName) {
ensQueries.push(queryClient.prefetchQuery(ensQuery(ensName)));
}
await Promise.all([
...ensQueries,
queryClient.prefetchQuery(publisherProfileQuery(address)),
queryClient.prefetchQuery({
queryKey: ["published-contracts", address],
queryFn: () => fetchPublishedContracts(polygonSdk, queryClient, address),
}),
]);
return {
props: {
dehydratedState: dehydrate(queryClient),
profileAddress: checksummedAddress,
},
};
};
export const getStaticPaths: GetStaticPaths = async () => {
export const getStaticPaths = async () => {
return {
fallback: true,
paths: [],
+4 -1
View File
@@ -159,7 +159,10 @@ export {
isUninstallModuleSupported,
type UninstallModuleParams,
} from "../extensions/modules/__generated__/IModularCore/write/uninstallModule.js";
export { getModuleConfig } from "../extensions/modules/__generated__/IModule/read/getModuleConfig.js";
export {
getModuleConfig,
isGetModuleConfigSupported,
} from "../extensions/modules/__generated__/IModule/read/getModuleConfig.js";
export {
installPublishedModule,
type InstallPublishedModuleOptions,
@@ -48,7 +48,6 @@ export function publishContract(
contract: options.contract,
async asyncParams() {
const currentVersion = options.previousMetadata?.version;
// check if the version is greater than the current version
if (
currentVersion &&
@@ -205,5 +205,9 @@ export type ExtendedMetadata = {
bytecodeUri: string;
}[]
>;
externalLinks?: Array<{
name: string;
url: string;
}>;
[key: string]: unknown;
};
-9
View File
@@ -227,15 +227,9 @@ importers:
'@tanstack/react-table':
specifier: ^8.17.3
version: 8.20.5([email protected]([email protected]))([email protected])
'@thirdweb-dev/sdk':
specifier: workspace:*
version: link:../../legacy_packages/sdk
'@thirdweb-dev/service-utils':
specifier: workspace:*
version: link:../../packages/service-utils
'@thirdweb-dev/storage':
specifier: workspace:*
version: link:../../legacy_packages/storage
'@vercel/og':
specifier: ^0.6.2
version: 0.6.2
@@ -260,9 +254,6 @@ importers:
date-fns:
specifier: ^3.6.0
version: 3.6.0
ethers:
specifier: 5.7.2
version: 5.7.2([email protected])([email protected])
flat:
specifier: ^6.0.1
version: 6.0.1