From 95b3c787a3d6fcdcd5b484d1e189f73a9e28e442 Mon Sep 17 00:00:00 2001 From: jnsdls Date: Mon, 16 Sep 2024 19:33:35 +0000 Subject: [PATCH] [Update] Refactor contract publish form to use thirdweb v5 (#4605) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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}` --- apps/dashboard/.eslintrc.js | 25 -- apps/dashboard/knip.json | 8 +- apps/dashboard/next-sitemap.config.js | 2 - apps/dashboard/package.json | 3 - apps/dashboard/src/@/constants/auth.ts | 2 + apps/dashboard/src/@/constants/env.ts | 4 + .../@3rdweb-sdk/react/hooks/useRegistry.ts | 2 +- .../contracts/publish/[publish_uri]/page.tsx | 47 ++ .../[publisher]/[contract_id]/page.tsx | 5 +- .../components/app-layouts/provider-setup.tsx | 48 -- .../contract-publish-form/default-factory.tsx | 45 +- .../dynamic-contract-fieldset.tsx | 46 -- .../contract-publish-form/extension-input.tsx | 119 ----- .../factory-fieldset.tsx | 18 +- .../contract-publish-form/index.tsx | 419 +++++++----------- .../landing-fieldset.tsx | 5 +- .../modular-contract-fieldset.tsx | 46 -- .../contract-publish-form/module-input.tsx | 118 ----- .../modules-param-selector.tsx | 41 -- .../contract-table-v2/index.tsx | 2 +- .../contract-table/cells/description.tsx | 15 +- .../contract-table/cells/name.tsx | 4 +- .../components/contract-components/hooks.ts | 416 +++-------------- .../publisher/edit-profile.tsx | 48 +- .../shared/contract-id-image.tsx | 28 +- .../shared/published-by.tsx | 10 +- .../contract-components/tables/cells.tsx | 9 +- .../contract-functions/contract-function.tsx | 123 ++--- .../contract-header/contract-metadata.tsx | 6 +- .../explore/contract-card/index.tsx | 23 +- .../smart-wallets/AccountFactories/index.tsx | 43 +- .../src/components/storage/your-files.tsx | 2 +- apps/dashboard/src/constants/chains.ts | 54 --- .../components/claim-conditions-form/hooks.ts | 8 +- .../claim-conditions-form/index.tsx | 11 +- .../claim-conditions/legacy-zod-schema.ts | 119 +++++ .../tabs/code/components/code-overview.tsx | 44 +- .../components/InstalledModulesTable.tsx | 2 +- .../tabs/manage/components/ModuleForm.tsx | 73 +-- .../manage/components/moduleContractInfo.ts | 11 +- .../src/contract-ui/tabs/nfts/page.tsx | 6 - .../src/lib/deployment/template-values.ts | 3 +- apps/dashboard/src/lib/ens.ts | 2 +- apps/dashboard/src/lib/sdk.ts | 96 ---- apps/dashboard/src/page-id.ts | 3 - .../src/pages/[chain_id]/[...paths].tsx | 5 +- .../src/pages/api/moralis/balances.ts | 3 +- .../src/pages/api/revalidate/publish.ts | 76 ---- .../src/pages/contracts/[...catchAll].tsx | 15 - .../pages/contracts/publish/[contractId].tsx | 25 -- .../src/pages/explore/[category]/index.tsx | 8 +- apps/dashboard/src/pages/explore/index.tsx | 8 +- .../src/pages/profile/[profileAddress].tsx | 63 +-- packages/thirdweb/src/exports/modules.ts | 5 +- .../src/extensions/thirdweb/write/publish.ts | 1 - .../src/utils/any-evm/deploy-metadata.ts | 4 + pnpm-lock.yaml | 9 - 57 files changed, 652 insertions(+), 1734 deletions(-) create mode 100644 apps/dashboard/src/app/(dashboard)/contracts/publish/[publish_uri]/page.tsx delete mode 100644 apps/dashboard/src/components/app-layouts/provider-setup.tsx delete mode 100644 apps/dashboard/src/components/contract-components/contract-publish-form/dynamic-contract-fieldset.tsx delete mode 100644 apps/dashboard/src/components/contract-components/contract-publish-form/extension-input.tsx delete mode 100644 apps/dashboard/src/components/contract-components/contract-publish-form/modular-contract-fieldset.tsx delete mode 100644 apps/dashboard/src/components/contract-components/contract-publish-form/module-input.tsx delete mode 100644 apps/dashboard/src/components/contract-components/contract-publish-form/modules-param-selector.tsx create mode 100644 apps/dashboard/src/contract-ui/tabs/claim-conditions/legacy-zod-schema.ts delete mode 100644 apps/dashboard/src/pages/api/revalidate/publish.ts delete mode 100644 apps/dashboard/src/pages/contracts/[...catchAll].tsx delete mode 100644 apps/dashboard/src/pages/contracts/publish/[contractId].tsx diff --git a/apps/dashboard/.eslintrc.js b/apps/dashboard/.eslintrc.js index 9517d27d5..fc9d8c926 100644 --- a/apps/dashboard/.eslintrc.js +++ b/apps/dashboard/.eslintrc.js @@ -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/**/*", diff --git a/apps/dashboard/knip.json b/apps/dashboard/knip.json index 725376d60..0c799902a 100644 --- a/apps/dashboard/knip.json +++ b/apps/dashboard/knip.json @@ -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"] } diff --git a/apps/dashboard/next-sitemap.config.js b/apps/dashboard/next-sitemap.config.js index a1bae86af..3bd01a3a4 100644 --- a/apps/dashboard/next-sitemap.config.js +++ b/apps/dashboard/next-sitemap.config.js @@ -1,7 +1,6 @@ // @ts-check /** * - * @returns {Promise} */ 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} */ async function getSingleChain(chainIdOrSlug) { const res = await fetch( diff --git a/apps/dashboard/package.json b/apps/dashboard/package.json index f27e6a25e..27c4251d5 100644 --- a/apps/dashboard/package.json +++ b/apps/dashboard/package.json @@ -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", diff --git a/apps/dashboard/src/@/constants/auth.ts b/apps/dashboard/src/@/constants/auth.ts index 2b9ca9a94..73e67e397 100644 --- a/apps/dashboard/src/@/constants/auth.ts +++ b/apps/dashboard/src/@/constants/auth.ts @@ -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) { diff --git a/apps/dashboard/src/@/constants/env.ts b/apps/dashboard/src/@/constants/env.ts index 3e467d44d..dd2ad7f87 100644 --- a/apps/dashboard/src/@/constants/env.ts +++ b/apps/dashboard/src/@/constants/env.ts @@ -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"; diff --git a/apps/dashboard/src/@3rdweb-sdk/react/hooks/useRegistry.ts b/apps/dashboard/src/@3rdweb-sdk/react/hooks/useRegistry.ts index 7b5b51879..7a566a52e 100644 --- a/apps/dashboard/src/@3rdweb-sdk/react/hooks/useRegistry.ts +++ b/apps/dashboard/src/@3rdweb-sdk/react/hooks/useRegistry.ts @@ -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 () => { diff --git a/apps/dashboard/src/app/(dashboard)/contracts/publish/[publish_uri]/page.tsx b/apps/dashboard/src/app/(dashboard)/contracts/publish/[publish_uri]/page.tsx new file mode 100644 index 000000000..4c72c8453 --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/contracts/publish/[publish_uri]/page.tsx @@ -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 ( +
+ + { + "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", + ); + }} + /> + +
+ ); +} diff --git a/apps/dashboard/src/app/(dashboard)/published-contract/[publisher]/[contract_id]/page.tsx b/apps/dashboard/src/app/(dashboard)/published-contract/[publisher]/[contract_id]/page.tsx index d86721a57..13838308f 100644 --- a/apps/dashboard/src/app/(dashboard)/published-contract/[publisher]/[contract_id]/page.tsx +++ b/apps/dashboard/src/app/(dashboard)/published-contract/[publisher]/[contract_id]/page.tsx @@ -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( {/* TODO: remove the chakra things :) */} - +
- +
); diff --git a/apps/dashboard/src/components/app-layouts/provider-setup.tsx b/apps/dashboard/src/components/app-layouts/provider-setup.tsx deleted file mode 100644 index ab75fe87d..000000000 --- a/apps/dashboard/src/components/app-layouts/provider-setup.tsx +++ /dev/null @@ -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(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; -} diff --git a/apps/dashboard/src/components/contract-components/contract-publish-form/default-factory.tsx b/apps/dashboard/src/components/contract-components/contract-publish-form/default-factory.tsx index aa24e1730..6da4b20ed 100644 --- a/apps/dashboard/src/components/contract-components/contract-publish-form/default-factory.tsx +++ b/apps/dashboard/src/components/contract-components/contract-publish-form/default-factory.tsx @@ -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 = ({ - abi, - shouldShowDynamicFactoryInput, - shouldShowModularFactoryInput, - shouldShowModulesParamInput, - deployParams, -}) => { +export const DefaultFactory: React.FC = ({ abi }) => { const form = useFormContext(); return ( @@ -74,36 +61,8 @@ export const DefaultFactory: React.FC = ({ } /> - - {shouldShowModulesParamInput && ( - <> - - Modules Param - - The contract uses modules. Choose the modules param name from - the initializer params below. - - - - - form.setValue( - "factoryDeploymentData.modularFactoryInput.modulesParamName", - selectedParam, - ) - } - /> - - - )} - {shouldShowDynamicFactoryInput && } - {shouldShowModularFactoryInput && } ); }; diff --git a/apps/dashboard/src/components/contract-components/contract-publish-form/dynamic-contract-fieldset.tsx b/apps/dashboard/src/components/contract-components/contract-publish-form/dynamic-contract-fieldset.tsx deleted file mode 100644 index 0a03e9c07..000000000 --- a/apps/dashboard/src/components/contract-components/contract-publish-form/dynamic-contract-fieldset.tsx +++ /dev/null @@ -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 ( - - - Contract extension settings - You can set default extensions for your contract. - - - {fields.map((item, index) => ( - - ))} - - - - - - ); -}; diff --git a/apps/dashboard/src/components/contract-components/contract-publish-form/extension-input.tsx b/apps/dashboard/src/components/contract-components/contract-publish-form/extension-input.tsx deleted file mode 100644 index d4cfd6d4b..000000000 --- a/apps/dashboard/src/components/contract-components/contract-publish-form/extension-input.tsx +++ /dev/null @@ -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 = ({ - 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 ( - - - - Publisher - - - - Extension Name - - - - - - - Extension Version - - - - - } - aria-label="Remove row" - onClick={() => remove(index)} - alignSelf="end" - /> - - - - ); -}; diff --git a/apps/dashboard/src/components/contract-components/contract-publish-form/factory-fieldset.tsx b/apps/dashboard/src/components/contract-components/contract-publish-form/factory-fieldset.tsx index d268aa02b..1688d389b 100644 --- a/apps/dashboard/src/components/contract-components/contract-publish-form/factory-fieldset.tsx +++ b/apps/dashboard/src/components/contract-components/contract-publish-form/factory-fieldset.tsx @@ -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>; - shouldShowDynamicFactoryInput: boolean; - shouldShowModularFactoryInput: boolean; - shouldShowModulesParamInput: boolean; - deployParams: readonly AbiParameter[]; } export const FactoryFieldset: React.FC = ({ abi, setCustomFactoryAbi, - shouldShowDynamicFactoryInput, - shouldShowModularFactoryInput, - shouldShowModulesParamInput, - deployParams, }) => { const form = useFormContext(); @@ -56,13 +48,7 @@ export const FactoryFieldset: React.FC = ({ {form.watch("deployType") === "autoFactory" && ( - + )} {form.watch("deployType") === "customFactory" && ( diff --git a/apps/dashboard/src/components/contract-components/contract-publish-form/index.tsx b/apps/dashboard/src/components/contract-components/contract-publish-form/index.tsx index f1539bd0c..f2ab6fb07 100644 --- a/apps/dashboard/src/components/contract-components/contract-publish-form/index.tsx +++ b/apps/dashboard/src/components/contract-components/contract-publish-form/index.tsx @@ -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 = ({ - contractId, -}) => { +export function ContractPublishForm(props: { + publishMetadata: FetchDeployMetadataResult; + onPublishSuccess: () => Promise; +}) { + useLoggedInUser(); const [customFactoryAbi, setCustomFactoryAbi] = useState([]); - 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 = ({ 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>({ + const form = useForm< + FetchDeployMetadataResult & { name: string; version: string } + >({ defaultValues: transformedQueryData, values: transformedQueryData, resetOptions: { @@ -173,61 +122,46 @@ export const ContractPublishForm: React.FC = ({ }, }); - const hasTrackedImpression = useRef(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 = ({ ? 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 = ({ 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 = ({ 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 = ({ {fieldsetToShow === "factory" && ( )} @@ -466,7 +345,7 @@ export const ContractPublishForm: React.FC = ({ flexDir={{ base: "column", md: "row" }} gap={4} > - {!address ? ( + {!account ? ( <> @@ -524,7 +403,7 @@ export const ContractPublishForm: React.FC = ({ form="contract-release-form" isDisabled={isDisabled} loadingText={ - publishMutation.isSuccess + sendTx.isSuccess ? "Preparing page" : "Publishing contract" } @@ -540,4 +419,4 @@ export const ContractPublishForm: React.FC = ({ ); -}; +} diff --git a/apps/dashboard/src/components/contract-components/contract-publish-form/landing-fieldset.tsx b/apps/dashboard/src/components/contract-components/contract-publish-form/landing-fieldset.tsx index 281f7d072..78864dec9 100644 --- a/apps/dashboard/src/components/contract-components/contract-publish-form/landing-fieldset.tsx +++ b/apps/dashboard/src/components/contract-components/contract-publish-form/landing-fieldset.tsx @@ -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 = ({ latestVersion, placeholderVersion, }) => { - const form = useFormContext(); + const form = useFormContext(); const logoUrl = useImageFileOrUrl(form.watch("logo")); const handleVersionChange = (e: React.ChangeEvent) => { @@ -95,6 +95,7 @@ export const LandingFieldset: React.FC = ({ form.setValue("logo", file)} className="border border-border rounded transition-all duration-200" renderPreview={(fileUrl) => ( diff --git a/apps/dashboard/src/components/contract-components/contract-publish-form/modular-contract-fieldset.tsx b/apps/dashboard/src/components/contract-components/contract-publish-form/modular-contract-fieldset.tsx deleted file mode 100644 index 37ffd4c1b..000000000 --- a/apps/dashboard/src/components/contract-components/contract-publish-form/modular-contract-fieldset.tsx +++ /dev/null @@ -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 ( - - - Contract module settings - You can set default modules for your contract. - - - {fields.map((item, index) => ( - - ))} - - - - - - ); -}; diff --git a/apps/dashboard/src/components/contract-components/contract-publish-form/module-input.tsx b/apps/dashboard/src/components/contract-components/contract-publish-form/module-input.tsx deleted file mode 100644 index b0fc2a11e..000000000 --- a/apps/dashboard/src/components/contract-components/contract-publish-form/module-input.tsx +++ /dev/null @@ -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 = ({ 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 ( - - - - Publisher - - - - Module Name - - - - - - - Module Version - - - - - } - aria-label="Remove row" - onClick={() => remove(index)} - alignSelf="end" - /> - - - - ); -}; diff --git a/apps/dashboard/src/components/contract-components/contract-publish-form/modules-param-selector.tsx b/apps/dashboard/src/components/contract-components/contract-publish-form/modules-param-selector.tsx deleted file mode 100644 index bbf30ef80..000000000 --- a/apps/dashboard/src/components/contract-components/contract-publish-form/modules-param-selector.tsx +++ /dev/null @@ -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 = ({ - deployParams, - value, - onChange, -}) => { - return ( - - ); -}; diff --git a/apps/dashboard/src/components/contract-components/contract-table-v2/index.tsx b/apps/dashboard/src/components/contract-components/contract-table-v2/index.tsx index 22993158f..4c6482492 100644 --- a/apps/dashboard/src/components/contract-components/contract-table-v2/index.tsx +++ b/apps/dashboard/src/components/contract-components/contract-table-v2/index.tsx @@ -47,7 +47,7 @@ function convertContractDataToRowData( input: ContractDataInput, ): ContractDataRow { return { - id: input.id, + id: input.contractId, ...input.metadata, }; } diff --git a/apps/dashboard/src/components/contract-components/contract-table/cells/description.tsx b/apps/dashboard/src/components/contract-components/contract-table/cells/description.tsx index 063222eea..91ee28574 100644 --- a/apps/dashboard/src/components/contract-components/contract-table/cells/description.tsx +++ b/apps/dashboard/src/components/contract-components/contract-table/cells/description.tsx @@ -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 ( - {fullPublishMetadata.data?.latestPublishedContractMetadata + {deployMetadataResultQuery.data?.latestPublishedContractMetadata ?.publishedMetadata.description || - (!fullPublishMetadata.isFetching ? "First Version" : "None")} + (!deployMetadataResultQuery.isFetching ? "First Version" : "None")} ); diff --git a/apps/dashboard/src/components/contract-components/contract-table/cells/name.tsx b/apps/dashboard/src/components/contract-components/contract-table/cells/name.tsx index 15b7614f9..5fcf6081a 100644 --- a/apps/dashboard/src/components/contract-components/contract-table/cells/name.tsx +++ b/apps/dashboard/src/components/contract-components/contract-table/cells/name.tsx @@ -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 ( diff --git a/apps/dashboard/src/components/contract-components/hooks.ts b/apps/dashboard/src/components/contract-components/hooks.ts index cd39de71f..0fc5e2d82 100644 --- a/apps/dashboard/src/components/contract-components/hooks.ts +++ b/apps/dashboard/src/components/contract-components/hooks.ts @@ -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) { - // biome-ignore lint/suspicious/noExplicitAny: FIXME - const newObj: Record = {}; +function isAnyObject(value: unknown): value is Record { + return typeof value === "object" && value !== null; +} + +function removeUndefinedFromObjectDeep>( + 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> | 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 >[number]; -export function usePublishedContractsQuery( - address?: string, - feature?: FeatureName, -) { - const queryClient = useQueryClient(); +export function usePublishedContractsQuery(address?: string) { return useQuery({ - 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, - 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") { diff --git a/apps/dashboard/src/components/contract-components/publisher/edit-profile.tsx b/apps/dashboard/src/components/contract-components/publisher/edit-profile.tsx index 27e6f882e..75d205325 100644 --- a/apps/dashboard/src/components/contract-components/publisher/edit-profile.tsx +++ b/apps/dashboard/src/components/contract-components/publisher/edit-profile.tsx @@ -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 = ({ 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 = ({
{ + 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 = ({ role="group" colorScheme="primary" type="submit" - isLoading={editProfile.isPending} + isLoading={sendTx.isPending} form={FORM_ID} > Save diff --git a/apps/dashboard/src/components/contract-components/shared/contract-id-image.tsx b/apps/dashboard/src/components/contract-components/shared/contract-id-image.tsx index 421c04bc4..efa6eec04 100644 --- a/apps/dashboard/src/components/contract-components/shared/contract-id-image.tsx +++ b/apps/dashboard/src/components/contract-components/shared/contract-id-image.tsx @@ -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 = ({ 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 ( - + {logo ? ( = ({ {...imgProps} boxSize={boxSize} src={img as StaticImageData} - alt={publishMetadata.data?.name || "Contract Image"} + alt={deployMetadataResultQuery.data?.name || "Contract Image"} /> ) : ( {publishMetadata.data?.name )} diff --git a/apps/dashboard/src/components/contract-components/shared/published-by.tsx b/apps/dashboard/src/components/contract-components/shared/published-by.tsx index 64f178411..119373631 100644 --- a/apps/dashboard/src/components/contract-components/shared/published-by.tsx +++ b/apps/dashboard/src/components/contract-components/shared/published-by.tsx @@ -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 = ({ 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; diff --git a/apps/dashboard/src/components/contract-components/tables/cells.tsx b/apps/dashboard/src/components/contract-components/tables/cells.tsx index 87c2c2a4f..934f6bb0b 100644 --- a/apps/dashboard/src/components/contract-components/tables/cells.tsx +++ b/apps/dashboard/src/components/contract-components/tables/cells.tsx @@ -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 = diff --git a/apps/dashboard/src/components/contract-functions/contract-function.tsx b/apps/dashboard/src/components/contract-functions/contract-function.tsx index 19a637101..292dcb63e 100644 --- a/apps/dashboard/src/components/contract-functions/contract-function.tsx +++ b/apps/dashboard/src/components/contract-functions/contract-function.tsx @@ -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(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 = ({ +const ContractFunction: React.FC> = ({ fn, contract, }) => { + if (!fn || !contract) { + return null; + } + + return ; +}; + +function ContractFunctionInner({ contract, fn }: ContractFunctionProps) { const [environment, setEnvironment] = useState("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 = ({ )} - {/* TODO: bring this back eventually */} - {/* {fn.comment && ( - - )} */} {fn.inputs?.length && !contract ? ( <> @@ -246,7 +207,7 @@ const ContractFunction: React.FC = ({ )} ); -}; +} interface ContractFunctionsPanelProps { fnsOrEvents: (AbiFunction | AbiEvent)[]; @@ -262,34 +223,16 @@ export const ContractFunctionsPanel: React.FC = ({ 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) => { diff --git a/apps/dashboard/src/components/custom-contract/contract-header/contract-metadata.tsx b/apps/dashboard/src/components/custom-contract/contract-header/contract-metadata.tsx index e87d9a76c..8a3b35101 100644 --- a/apps/dashboard/src/components/custom-contract/contract-header/contract-metadata.tsx +++ b/apps/dashboard/src/components/custom-contract/contract-header/contract-metadata.tsx @@ -16,10 +16,8 @@ export const ContractMetadata: React.FC = ({ }) => { 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 diff --git a/apps/dashboard/src/components/explore/contract-card/index.tsx b/apps/dashboard/src/components/explore/contract-card/index.tsx index 8a5add45e..4e59fc294 100644 --- a/apps/dashboard/src/components/explore/contract-card/index.tsx +++ b/apps/dashboard/src/components/explore/contract-card/index.tsx @@ -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}`, }; } diff --git a/apps/dashboard/src/components/smart-wallets/AccountFactories/index.tsx b/apps/dashboard/src/components/smart-wallets/AccountFactories/index.tsx index c1ec193dd..bf2649e37 100644 --- a/apps/dashboard/src/components/smart-wallets/AccountFactories/index.tsx +++ b/apps/dashboard/src/components/smart-wallets/AccountFactories/index.tsx @@ -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, }); }; diff --git a/apps/dashboard/src/components/storage/your-files.tsx b/apps/dashboard/src/components/storage/your-files.tsx index e4b9e3846..230b17b17 100644 --- a/apps/dashboard/src/components/storage/your-files.tsx +++ b/apps/dashboard/src/components/storage/your-files.tsx @@ -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"; diff --git a/apps/dashboard/src/constants/chains.ts b/apps/dashboard/src/constants/chains.ts index 4f5294901..b18bb4c03 100644 --- a/apps/dashboard/src/constants/chains.ts +++ b/apps/dashboard/src/constants/chains.ts @@ -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; diff --git a/apps/dashboard/src/contract-ui/tabs/claim-conditions/components/claim-conditions-form/hooks.ts b/apps/dashboard/src/contract-ui/tabs/claim-conditions/components/claim-conditions-form/hooks.ts index 17c0c9d07..f1560b506 100644 --- a/apps/dashboard/src/contract-ui/tabs/claim-conditions/components/claim-conditions-form/hooks.ts +++ b/apps/dashboard/src/contract-ui/tabs/claim-conditions/components/claim-conditions-form/hooks.ts @@ -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; diff --git a/apps/dashboard/src/contract-ui/tabs/claim-conditions/components/claim-conditions-form/index.tsx b/apps/dashboard/src/contract-ui/tabs/claim-conditions/components/claim-conditions-form/index.tsx index 48c18728a..9fa93bf66 100644 --- a/apps/dashboard/src/contract-ui/tabs/claim-conditions/components/claim-conditions-form/index.tsx +++ b/apps/dashboard/src/contract-ui/tabs/claim-conditions/components/claim-conditions-form/index.tsx @@ -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 = ({ return (claimConditionsQuery.data || []) .map((phase, idx) => ({ ...phase, + merkleRootHash: (phase.merkleRootHash || "") as string, price: phase.currencyMetadata.displayValue, maxClaimableSupply: phase.maxClaimableSupply?.toString() || "0", currencyMetadata: { diff --git a/apps/dashboard/src/contract-ui/tabs/claim-conditions/legacy-zod-schema.ts b/apps/dashboard/src/contract-ui/tabs/claim-conditions/legacy-zod-schema.ts new file mode 100644 index 000000000..130fc651c --- /dev/null +++ b/apps/dashboard/src/contract-ui/tabs/claim-conditions/legacy-zod-schema.ts @@ -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; + +export type ClaimConditionInput = z.input< + typeof PartialClaimConditionInputSchema +>; + +export type ClaimCondition = z.output; diff --git a/apps/dashboard/src/contract-ui/tabs/code/components/code-overview.tsx b/apps/dashboard/src/contract-ui/tabs/code/components/code-overview.tsx index d41e89259..7ab851a7a 100644 --- a/apps/dashboard/src/contract-ui/tabs/code/components/code-overview.tsx +++ b/apps/dashboard/src/contract-ui/tabs/code/components/code-overview.tsx @@ -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 = ({ 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); diff --git a/apps/dashboard/src/contract-ui/tabs/manage/components/InstalledModulesTable.tsx b/apps/dashboard/src/contract-ui/tabs/manage/components/InstalledModulesTable.tsx index f53736c2b..84c95d686 100644 --- a/apps/dashboard/src/contract-ui/tabs/manage/components/InstalledModulesTable.tsx +++ b/apps/dashboard/src/contract-ui/tabs/manage/components/InstalledModulesTable.tsx @@ -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) => { diff --git a/apps/dashboard/src/contract-ui/tabs/manage/components/ModuleForm.tsx b/apps/dashboard/src/contract-ui/tabs/manage/components/ModuleForm.tsx index 66582f9fe..9c9c515b2 100644 --- a/apps/dashboard/src/contract-ui/tabs/manage/components/ModuleForm.tsx +++ b/apps/dashboard/src/contract-ui/tabs/manage/components/ModuleForm.tsx @@ -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) => { > Module Name @@ -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) { diff --git a/apps/dashboard/src/contract-ui/tabs/manage/components/moduleContractInfo.ts b/apps/dashboard/src/contract-ui/tabs/manage/components/moduleContractInfo.ts index 8ab0150a2..f65ea67c6 100644 --- a/apps/dashboard/src/contract-ui/tabs/manage/components/moduleContractInfo.ts +++ b/apps/dashboard/src/contract-ui/tabs/manage/components/moduleContractInfo.ts @@ -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 = [ diff --git a/apps/dashboard/src/contract-ui/tabs/nfts/page.tsx b/apps/dashboard/src/contract-ui/tabs/nfts/page.tsx index 63e5fe4b3..bf4ca1bd5 100644 --- a/apps/dashboard/src/contract-ui/tabs/nfts/page.tsx +++ b/apps/dashboard/src/contract-ui/tabs/nfts/page.tsx @@ -72,12 +72,6 @@ export const ContractNFTPage: React.FC = ({ const canCreateDelayedRevealBatch = ERC721Ext.isCreateDelayedRevealBatchSupported(functionSelectorQuery.data); - console.log("***DEBUG*** canRenderNFTTable: ", { - isErc721, - canRenderNFTTable, - functionSelectorQuery, - }); - return (
diff --git a/apps/dashboard/src/lib/deployment/template-values.ts b/apps/dashboard/src/lib/deployment/template-values.ts index d4fa82f65..ed36f7b11 100644 --- a/apps/dashboard/src/lib/deployment/template-values.ts +++ b/apps/dashboard/src/lib/deployment/template-values.ts @@ -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 { diff --git a/apps/dashboard/src/lib/ens.ts b/apps/dashboard/src/lib/ens.ts index 6fddf31d8..183827462 100644 --- a/apps/dashboard/src/lib/ens.ts +++ b/apps/dashboard/src/lib/ens.ts @@ -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; } diff --git a/apps/dashboard/src/lib/sdk.ts b/apps/dashboard/src/lib/sdk.ts index aa1c0305a..34ed4d3e1 100644 --- a/apps/dashboard/src/lib/sdk.ts +++ b/apps/dashboard/src/lib/sdk.ts @@ -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(); - -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, - }, - }); -} diff --git a/apps/dashboard/src/page-id.ts b/apps/dashboard/src/page-id.ts index c8d3647c8..1c50578c9 100644 --- a/apps/dashboard/src/page-id.ts +++ b/apps/dashboard/src/page-id.ts @@ -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", diff --git a/apps/dashboard/src/pages/[chain_id]/[...paths].tsx b/apps/dashboard/src/pages/[chain_id]/[...paths].tsx index ba6e034eb..84f222ba6 100644 --- a/apps/dashboard/src/pages/[chain_id]/[...paths].tsx +++ b/apps/dashboard/src/pages/[chain_id]/[...paths].tsx @@ -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 = async (ctx) => { return { props: { - dehydratedState: dehydrate(queryClient), + dehydratedState: dehydrate(queryClient, { + serializeData: (d) => stringify(d), + }), contractInfo: { chainSlug, contractAddress: checksummedAddress, diff --git a/apps/dashboard/src/pages/api/moralis/balances.ts b/apps/dashboard/src/pages/api/moralis/balances.ts index 9fce4e3d2..917e3d8e9 100644 --- a/apps/dashboard/src/pages/api/moralis/balances.ts +++ b/apps/dashboard/src/pages/api/moralis/balances.ts @@ -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; }; diff --git a/apps/dashboard/src/pages/api/revalidate/publish.ts b/apps/dashboard/src/pages/api/revalidate/publish.ts deleted file mode 100644 index 270e8556c..000000000 --- a/apps/dashboard/src/pages/api/revalidate/publish.ts +++ /dev/null @@ -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; diff --git a/apps/dashboard/src/pages/contracts/[...catchAll].tsx b/apps/dashboard/src/pages/contracts/[...catchAll].tsx deleted file mode 100644 index 947a38f83..000000000 --- a/apps/dashboard/src/pages/contracts/[...catchAll].tsx +++ /dev/null @@ -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, - }, - }; -}; diff --git a/apps/dashboard/src/pages/contracts/publish/[contractId].tsx b/apps/dashboard/src/pages/contracts/publish/[contractId].tsx deleted file mode 100644 index a5485ae3c..000000000 --- a/apps/dashboard/src/pages/contracts/publish/[contractId].tsx +++ /dev/null @@ -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 ( - - {contractId && } - - ); -}; - -ContractsPublishPage.getLayout = (page, props) => ( - {page} -); - -ContractsPublishPage.pageId = PageId.PublishSingle; - -export default ContractsPublishPage; diff --git a/apps/dashboard/src/pages/explore/[category]/index.tsx b/apps/dashboard/src/pages/explore/[category]/index.tsx index 382e9fb5d..fdf2e0104 100644 --- a/apps/dashboard/src/pages/explore/[category]/index.tsx +++ b/apps/dashboard/src/pages/explore/[category]/index.tsx @@ -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 = async (ctx) => { await prefetchCategory(category, queryClient); return { - props: { category, dehydratedState: dehydrate(queryClient) }, + props: { + category, + dehydratedState: dehydrate(queryClient, { + serializeData: (d) => stringify(d), + }), + }, }; }; diff --git a/apps/dashboard/src/pages/explore/index.tsx b/apps/dashboard/src/pages/explore/index.tsx index a16bdf2b6..f8b3d029e 100644 --- a/apps/dashboard/src/pages/explore/index.tsx +++ b/apps/dashboard/src/pages/explore/index.tsx @@ -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 = 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), + }), + }, }; }; diff --git a/apps/dashboard/src/pages/profile/[profileAddress].tsx b/apps/dashboard/src/pages/profile/[profileAddress].tsx index f3f3dd885..055cd28c1 100644 --- a/apps/dashboard/src/pages/profile/[profileAddress].tsx +++ b/apps/dashboard/src/pages/profile/[profileAddress].tsx @@ -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 = 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 = 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: [], diff --git a/packages/thirdweb/src/exports/modules.ts b/packages/thirdweb/src/exports/modules.ts index 3001b5ed4..f5a6aedf8 100644 --- a/packages/thirdweb/src/exports/modules.ts +++ b/packages/thirdweb/src/exports/modules.ts @@ -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, diff --git a/packages/thirdweb/src/extensions/thirdweb/write/publish.ts b/packages/thirdweb/src/extensions/thirdweb/write/publish.ts index e7f782bbb..d7c321cb2 100644 --- a/packages/thirdweb/src/extensions/thirdweb/write/publish.ts +++ b/packages/thirdweb/src/extensions/thirdweb/write/publish.ts @@ -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 && diff --git a/packages/thirdweb/src/utils/any-evm/deploy-metadata.ts b/packages/thirdweb/src/utils/any-evm/deploy-metadata.ts index 9bc2113f8..d1d652441 100644 --- a/packages/thirdweb/src/utils/any-evm/deploy-metadata.ts +++ b/packages/thirdweb/src/utils/any-evm/deploy-metadata.ts @@ -205,5 +205,9 @@ export type ExtendedMetadata = { bytecodeUri: string; }[] >; + externalLinks?: Array<{ + name: string; + url: string; + }>; [key: string]: unknown; }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 451a19921..4b48a4aae 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -227,15 +227,9 @@ importers: '@tanstack/react-table': specifier: ^8.17.3 version: 8.20.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@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(bufferutil@4.0.8)(utf-8-validate@5.0.10) flat: specifier: ^6.0.1 version: 6.0.1