From ece86ccdc39862edef0c6dac916d3066853a9143 Mon Sep 17 00:00:00 2001 From: jnsdls Date: Fri, 13 Sep 2024 19:16:20 +0000 Subject: [PATCH] v5 deploy dashboard changes (#4498) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## PR-Codex overview This PR deprecates functions, updates types, refactors components, and adds modules for contract deployment. ### Detailed summary - Deprecated functions in favor of new ones - Updated types and added necessary type assertions - Refactored components with new props and imports - Added modules for contract deployment and handling metadata > The following files were skipped due to too many changes: `apps/dashboard/src/app/(dashboard)/published-contract/utils/module-base-64.ts`, `apps/dashboard/src/app/(dashboard)/published-contract/[publisher]/[contract_id]/components/module-list.client.tsx`, `apps/dashboard/src/app/(dashboard)/published-contract/components/uri-based-deploy.tsx`, `pnpm-lock.yaml`, `apps/dashboard/src/app/(dashboard)/published-contract/components/contract-actions-deploy.client.tsx`, `apps/dashboard/src/app/(dashboard)/published-contract/components/contract-info.tsx`, `apps/dashboard/src/app/(dashboard)/published-contract/components/contract-actions-published.client.tsx`, `packages/thirdweb/src/utils/any-evm/deploy-metadata.ts`, `apps/dashboard/src/app/(dashboard)/published-contract/[publisher]/[contract_id]/utils/getPublishedContractsWithPublisherMapping.ts`, `packages/thirdweb/package.json`, `apps/dashboard/src/app/(dashboard)/contracts/deploy/[compiler_uri]/page.tsx`, `apps/dashboard/src/contract-ui/tabs/code/components/code-overview.tsx`, `apps/dashboard/src/components/contract-components/contract-deploy-form/trusted-forwarders-fieldset.tsx`, `apps/dashboard/src/app/(dashboard)/published-contract/[publisher]/[contract_id]/layout.tsx`, `apps/dashboard/src/components/contract-components/contract-deploy-form/contract-metadata-fieldset.tsx`, `apps/dashboard/src/app/(dashboard)/published-contract/components/contract-header.tsx`, `apps/dashboard/src/app/(dashboard)/published-contract/[publisher]/[contract_id]/components/breadcrumbs.client.tsx`, `apps/dashboard/src/app/(dashboard)/published-contract/[publisher]/[contract_id]/opengraph-image.tsx`, `apps/dashboard/src/app/(dashboard)/published-contract/[publisher]/[contract_id]/page.tsx`, `apps/dashboard/src/app/(dashboard)/published-contract/[publisher]/[contract_id]/[version]/opengraph-image.tsx`, `packages/thirdweb/src/utils/abi/normalizeFunctionParams.ts`, `apps/dashboard/src/app/(dashboard)/published-contract/components/version-selector.tsx`, `packages/thirdweb/src/contract/deployment/deploy-with-abi.ts`, `apps/dashboard/src/contract-ui/tabs/manage/components/getModuleInstalledParams.ts`, `packages/thirdweb/src/extensions/prebuilts/get-required-transactions.test.ts`, `apps/dashboard/src/app/(dashboard)/published-contract/[publisher]/[contract_id]/[version]/page.tsx`, `apps/dashboard/src/app/(dashboard)/published-contract/components/publish-based-deploy.tsx`, `apps/dashboard/src/components/contract-components/fetch-contracts-with-versions.ts`, `apps/dashboard/src/components/contract-functions/contract-function.tsx`, `apps/dashboard/src/components/contract-components/contract-deploy-form/modular-contract-default-modules-fieldset.tsx`, `packages/thirdweb/src/extensions/prebuilts/get-required-transactions.ts`, `apps/dashboard/src/pages/api/og/publishedContract.tsx`, `apps/dashboard/src/components/contract-components/published-contract/index.tsx`, `apps/dashboard/src/components/contract-components/hooks.ts`, `apps/dashboard/src/components/contract-components/contract-deploy-form/custom-contract.tsx` > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` --- apps/dashboard/src/@/constants/auth.ts | 2 - .../contracts/deploy/[compiler_uri]/page.tsx | 34 + apps/dashboard/src/app/(dashboard)/layout.tsx | 5 +- .../[contract_id]/[version]/deploy/page.tsx | 24 + .../[version]/opengraph-image.tsx | 56 + .../[contract_id]/[version]/page.tsx | 76 + .../components/breadcrumbs.client.tsx | 56 + .../components/module-list.client.tsx | 36 + .../[publisher]/[contract_id]/deploy/page.tsx | 25 + .../[publisher]/[contract_id]/layout.tsx | 35 + .../[contract_id]/opengraph-image.tsx | 53 + .../[publisher]/[contract_id]/page.tsx | 54 + ...tPublishedContractsWithPublisherMapping.ts | 34 + .../publishedContractOGImageTemplate.tsx} | 171 +- .../contract-actions-deploy.client.tsx | 41 + .../contract-actions-published.client.tsx | 40 + .../components/contract-header.tsx | 45 + .../components/contract-info.tsx | 38 + .../components/publish-based-deploy.tsx | 89 ++ .../components/uri-based-deploy.tsx | 26 + .../components/version-selector.tsx | 76 + .../(dashboard)/published-contract/page.tsx | 28 + .../utils/module-base-64.ts | 36 + .../(dashboard)/support/components/product.ts | 12 - .../components/analytics/auto-bar-chart.tsx | 247 --- .../components/analytics/chart-container.tsx | 43 - .../components/analytics/stack-tooltip.tsx | 51 - .../contract-metadata-fieldset.tsx | 7 +- .../contract-deploy-form/custom-contract.tsx | 1379 ++++++++--------- .../contract-deploy-form/deploy-form-utils.ts | 8 - .../contract-deploy-form/index.tsx | 69 - ...ular-contract-default-modules-fieldset.tsx | 96 +- .../trusted-forwarders-fieldset.tsx | 21 - .../fetch-contracts-with-versions.ts | 110 ++ .../components/contract-components/hooks.ts | 650 +------- .../published-contract/index.tsx | 407 ++--- .../contract-functions/contract-function.tsx | 40 +- .../interactive-abi-function.tsx | 3 +- .../explore/contract-card/index.tsx | 5 +- .../components/getting-started/box/index.tsx | 40 - .../components/getting-started/card/index.tsx | 56 - .../src/components/icons/DotNetIcon.tsx | 32 - .../src/components/icons/ReactIcon.tsx | 22 - .../src/components/icons/TypeScriptIcon.tsx | 24 - .../src/components/icons/UnityIcon.tsx | 19 - .../src/components/icons/UnrealIcon.tsx | 15 - .../src/components/pages/publish.tsx | 250 --- .../src/components/share-buttom/index.tsx | 47 - .../src/contexts/custom-sdk-context.tsx | 2 +- apps/dashboard/src/contexts/error-handler.tsx | 2 + .../tabs/code/components/code-overview.tsx | 10 +- .../components/getModuleInstalledParams.ts | 45 +- .../components/install-module-params.tsx | 5 +- .../tabs/overview/components/Extensions.tsx | 52 - .../src/core-ui/batch-upload/progress-box.tsx | 48 - apps/dashboard/src/hooks/useQueryParam.ts | 11 +- apps/dashboard/src/lib/vercel-utils.ts | 2 +- apps/dashboard/src/middleware.ts | 2 +- apps/dashboard/src/og-lib/url-utils.ts | 7 - apps/dashboard/src/page-id.ts | 7 - .../pages/contracts/deploy/[contractId].tsx | 199 --- .../src/pages/publish/[...paths].tsx | 173 --- apps/dashboard/src/utils/router.ts | 3 + .../src/components/social/social-profiles.tsx | 1 + packages/thirdweb/package.json | 2 +- .../contract/deployment/deploy-with-abi.ts | 25 +- packages/thirdweb/src/exports/deploys.ts | 2 +- .../extensions/prebuilts/deploy-published.ts | 8 +- .../get-required-transactions.test.ts | 24 +- .../prebuilts/get-required-transactions.ts | 181 ++- packages/thirdweb/src/gas/fee-data.ts | 6 +- .../src/utils/abi/normalizeFunctionParams.ts | 43 +- .../src/utils/any-evm/deploy-metadata.ts | 35 +- .../any-evm/get-init-bytecode-with-salt.ts | 4 +- .../src/utils/contract/parse-abi-params.ts | 3 + pnpm-lock.yaml | 4 +- 76 files changed, 2332 insertions(+), 3307 deletions(-) create mode 100644 apps/dashboard/src/app/(dashboard)/contracts/deploy/[compiler_uri]/page.tsx create mode 100644 apps/dashboard/src/app/(dashboard)/published-contract/[publisher]/[contract_id]/[version]/deploy/page.tsx create mode 100644 apps/dashboard/src/app/(dashboard)/published-contract/[publisher]/[contract_id]/[version]/opengraph-image.tsx create mode 100644 apps/dashboard/src/app/(dashboard)/published-contract/[publisher]/[contract_id]/[version]/page.tsx create mode 100644 apps/dashboard/src/app/(dashboard)/published-contract/[publisher]/[contract_id]/components/breadcrumbs.client.tsx create mode 100644 apps/dashboard/src/app/(dashboard)/published-contract/[publisher]/[contract_id]/components/module-list.client.tsx create mode 100644 apps/dashboard/src/app/(dashboard)/published-contract/[publisher]/[contract_id]/deploy/page.tsx create mode 100644 apps/dashboard/src/app/(dashboard)/published-contract/[publisher]/[contract_id]/layout.tsx create mode 100644 apps/dashboard/src/app/(dashboard)/published-contract/[publisher]/[contract_id]/opengraph-image.tsx create mode 100644 apps/dashboard/src/app/(dashboard)/published-contract/[publisher]/[contract_id]/page.tsx create mode 100644 apps/dashboard/src/app/(dashboard)/published-contract/[publisher]/[contract_id]/utils/getPublishedContractsWithPublisherMapping.ts rename apps/dashboard/src/{pages/api/og/publishedContract.tsx => app/(dashboard)/published-contract/[publisher]/[contract_id]/utils/publishedContractOGImageTemplate.tsx} (70%) create mode 100644 apps/dashboard/src/app/(dashboard)/published-contract/components/contract-actions-deploy.client.tsx create mode 100644 apps/dashboard/src/app/(dashboard)/published-contract/components/contract-actions-published.client.tsx create mode 100644 apps/dashboard/src/app/(dashboard)/published-contract/components/contract-header.tsx create mode 100644 apps/dashboard/src/app/(dashboard)/published-contract/components/contract-info.tsx create mode 100644 apps/dashboard/src/app/(dashboard)/published-contract/components/publish-based-deploy.tsx create mode 100644 apps/dashboard/src/app/(dashboard)/published-contract/components/uri-based-deploy.tsx create mode 100644 apps/dashboard/src/app/(dashboard)/published-contract/components/version-selector.tsx create mode 100644 apps/dashboard/src/app/(dashboard)/published-contract/page.tsx create mode 100644 apps/dashboard/src/app/(dashboard)/published-contract/utils/module-base-64.ts delete mode 100644 apps/dashboard/src/app/(dashboard)/support/components/product.ts delete mode 100644 apps/dashboard/src/components/analytics/auto-bar-chart.tsx delete mode 100644 apps/dashboard/src/components/analytics/chart-container.tsx delete mode 100644 apps/dashboard/src/components/analytics/stack-tooltip.tsx delete mode 100644 apps/dashboard/src/components/contract-components/contract-deploy-form/deploy-form-utils.ts delete mode 100644 apps/dashboard/src/components/contract-components/contract-deploy-form/index.tsx create mode 100644 apps/dashboard/src/components/contract-components/fetch-contracts-with-versions.ts delete mode 100644 apps/dashboard/src/components/getting-started/box/index.tsx delete mode 100644 apps/dashboard/src/components/getting-started/card/index.tsx delete mode 100644 apps/dashboard/src/components/icons/DotNetIcon.tsx delete mode 100644 apps/dashboard/src/components/icons/ReactIcon.tsx delete mode 100644 apps/dashboard/src/components/icons/TypeScriptIcon.tsx delete mode 100644 apps/dashboard/src/components/icons/UnityIcon.tsx delete mode 100644 apps/dashboard/src/components/icons/UnrealIcon.tsx delete mode 100644 apps/dashboard/src/components/pages/publish.tsx delete mode 100644 apps/dashboard/src/components/share-buttom/index.tsx delete mode 100644 apps/dashboard/src/contract-ui/tabs/overview/components/Extensions.tsx delete mode 100644 apps/dashboard/src/core-ui/batch-upload/progress-box.tsx delete mode 100644 apps/dashboard/src/pages/contracts/deploy/[contractId].tsx delete mode 100644 apps/dashboard/src/pages/publish/[...paths].tsx diff --git a/apps/dashboard/src/@/constants/auth.ts b/apps/dashboard/src/@/constants/auth.ts index 3855755d8..2b9ca9a94 100644 --- a/apps/dashboard/src/@/constants/auth.ts +++ b/apps/dashboard/src/@/constants/auth.ts @@ -6,8 +6,6 @@ const LOGGED_IN_ONLY_PATHS = [ // anything that _starts_ with /cli is logged in only "/cli", "/support", - - // TODO: add any other logged in only paths here ]; export function isLoginRequired(pathname: string) { diff --git a/apps/dashboard/src/app/(dashboard)/contracts/deploy/[compiler_uri]/page.tsx b/apps/dashboard/src/app/(dashboard)/contracts/deploy/[compiler_uri]/page.tsx new file mode 100644 index 000000000..e6d8e2edf --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/contracts/deploy/[compiler_uri]/page.tsx @@ -0,0 +1,34 @@ +import { setOverrides } from "lib/vercel-utils"; +import { fetchDeployMetadata } from "thirdweb/contract"; +import { thirdwebClient } from "../../../../../@/constants/client"; +import { DeployContractInfo } from "../../../published-contract/components/contract-info"; +import { DeployFormForUri } from "../../../published-contract/components/uri-based-deploy"; + +setOverrides(); + +type DirectDeployPageProps = { + params: { + compiler_uri: string; + }; +}; + +export default async function DirectDeployPage(props: DirectDeployPageProps) { + const metadata = await fetchDeployMetadata({ + client: thirdwebClient, + // force `ipfs://` prefix + uri: props.params.compiler_uri.startsWith("ipfs://") + ? props.params.compiler_uri + : `ipfs://${props.params.compiler_uri}`, + }); + return ( +
+ + +
+ ); +} diff --git a/apps/dashboard/src/app/(dashboard)/layout.tsx b/apps/dashboard/src/app/(dashboard)/layout.tsx index 0f7e8d6e8..5e401b5ce 100644 --- a/apps/dashboard/src/app/(dashboard)/layout.tsx +++ b/apps/dashboard/src/app/(dashboard)/layout.tsx @@ -1,14 +1,15 @@ import { AppFooter } from "@/components/blocks/app-footer"; import { DashboardHeader } from "app/components/Header/DashboardHeader"; +import { ErrorProvider } from "../../contexts/error-handler"; export default function DashboardLayout(props: { children: React.ReactNode }) { return ( - <> +
{props.children}
- +
); } diff --git a/apps/dashboard/src/app/(dashboard)/published-contract/[publisher]/[contract_id]/[version]/deploy/page.tsx b/apps/dashboard/src/app/(dashboard)/published-contract/[publisher]/[contract_id]/[version]/deploy/page.tsx new file mode 100644 index 000000000..33f73a6db --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/published-contract/[publisher]/[contract_id]/[version]/deploy/page.tsx @@ -0,0 +1,24 @@ +import { setOverrides } from "lib/vercel-utils"; +import { DeployFormForPublishInfo } from "../../../../components/publish-based-deploy"; +import { moduleFromBase64 } from "../../../../utils/module-base-64"; + +setOverrides(); + +export default function PublishedContractVersionDeployPage({ + params, + searchParams, +}: { + params: { + publisher: string; + contract_id: string; + version: string; + }; + searchParams: { + module?: string[]; + }; +}) { + const modules = searchParams.module + ?.map((m) => moduleFromBase64(m)) + .filter((m) => m !== null); + return ; +} diff --git a/apps/dashboard/src/app/(dashboard)/published-contract/[publisher]/[contract_id]/[version]/opengraph-image.tsx b/apps/dashboard/src/app/(dashboard)/published-contract/[publisher]/[contract_id]/[version]/opengraph-image.tsx new file mode 100644 index 000000000..b00c1569b --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/published-contract/[publisher]/[contract_id]/[version]/opengraph-image.tsx @@ -0,0 +1,56 @@ +import { fetchPublisherProfile } from "components/contract-components/fetch-contracts-with-versions"; +import { format } from "date-fns/format"; +import { correctAndUniqueLicenses } from "lib/licenses"; +import { setOverrides } from "lib/vercel-utils"; +import { getPublishedContractsWithPublisherMapping } from "../utils/getPublishedContractsWithPublisherMapping"; +import { publishedContractOGImageTemplate } from "../utils/publishedContractOGImageTemplate"; + +export const runtime = "edge"; + +export const size = { + width: 1200, + height: 630, +}; + +export default async function Image(props: { + params: { + publisher: string; + contract_id: string; + version: string; + }; +}) { + setOverrides(); + const { publisher, contract_id } = props.params; + + const [publishedContracts, publisherProfile] = await Promise.all([ + getPublishedContractsWithPublisherMapping({ + publisher: publisher, + contract_id: contract_id, + }), + fetchPublisherProfile(publisher), + ]); + + const publishedContract = + publishedContracts.find((p) => p.version === props.params.version) || + publishedContracts[0]; + + const publishedContractName = + publishedContract?.displayName || publishedContract?.name; + + return publishedContractOGImageTemplate({ + title: publishedContractName, + description: publishedContract.description, + version: publishedContract.version || "latest", + publisher: publisherProfile?.name || publisher, + license: correctAndUniqueLicenses(publishedContract.licenses), + publishDate: format( + new Date( + Number.parseInt(publishedContract.publishTimestamp.toString() || "0") * + 1000, + ), + "MMM dd, yyyy", + ), + logo: publishedContract.logo, + publisherAvatar: publisherProfile?.avatar, + }); +} diff --git a/apps/dashboard/src/app/(dashboard)/published-contract/[publisher]/[contract_id]/[version]/page.tsx b/apps/dashboard/src/app/(dashboard)/published-contract/[publisher]/[contract_id]/[version]/page.tsx new file mode 100644 index 000000000..3887c7938 --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/published-contract/[publisher]/[contract_id]/[version]/page.tsx @@ -0,0 +1,76 @@ +import { Separator } from "@/components/ui/separator"; +import { thirdwebClient } from "@/constants/client"; +import { SimpleGrid } from "@chakra-ui/react"; +import { isAddress } from "thirdweb"; +import { resolveAddress } from "thirdweb/extensions/ens"; +import { ChakraProviderSetup } from "../../../../../../@/components/ChakraProviderSetup"; +import { fetchPublishedContractVersions } from "../../../../../../components/contract-components/fetch-contracts-with-versions"; +import { PublishedContract } from "../../../../../../components/contract-components/published-contract"; +import { setOverrides } from "../../../../../../lib/vercel-utils"; +import { PublishedActions } from "../../../components/contract-actions-published.client"; +import { DeployContractHeader } from "../../../components/contract-header"; + +setOverrides(); + +function mapThirdwebPublisher(publisher: string) { + if (publisher === "thirdweb.eth") { + return "deployer.thirdweb.eth"; + } + return publisher; +} + +type PublishedContractDeployPageProps = { + params: { + publisher: string; + contract_id: string; + version: string; + }; +}; + +export default async function PublishedContractPage( + props: PublishedContractDeployPageProps, +) { + // resolve ENS if required + const publisherAddress = isAddress(props.params.publisher) + ? props.params.publisher + : await resolveAddress({ + client: thirdwebClient, + name: mapThirdwebPublisher(props.params.publisher), + }); + + // get all the published versions of the contract + const publishedContractVersions = await fetchPublishedContractVersions( + publisherAddress, + props.params.contract_id, + ); + + // determine the "active" version of the contract based on the version that is passed + const publishedContract = + publishedContractVersions.find((v) => v.version === props.params.version) || + publishedContractVersions[0]; + + return ( + <> + + + + + {/* TODO: remove the chakra things :) */} + + + + + + + ); +} diff --git a/apps/dashboard/src/app/(dashboard)/published-contract/[publisher]/[contract_id]/components/breadcrumbs.client.tsx b/apps/dashboard/src/app/(dashboard)/published-contract/[publisher]/[contract_id]/components/breadcrumbs.client.tsx new file mode 100644 index 000000000..ccfe8e395 --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/published-contract/[publisher]/[contract_id]/components/breadcrumbs.client.tsx @@ -0,0 +1,56 @@ +"use client"; + +import { + Breadcrumb, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbList, + BreadcrumbPage, + BreadcrumbSeparator, +} from "@/components/ui/breadcrumb"; +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import { Fragment } from "react"; +import { isAddress } from "thirdweb"; +import { shortenAddress } from "thirdweb/utils"; + +export function PublishedContractBreadcrumbs() { + const pathname = usePathname() || ""; + const segments = pathname.split("/").filter((segment) => segment !== ""); + + return ( + + + + + Explore + + + + + {segments.map((segment, index) => { + const href = `/${segments.slice(0, index + 1).join("/")}`; + const isLast = index === segments.length - 1; + const renderedSegment = isAddress(segment) + ? shortenAddress(segment) + : segment; + + return ( + + + {isLast ? ( + {renderedSegment} + ) : ( + + {renderedSegment} + + )} + + {!isLast && } + + ); + })} + + + ); +} diff --git a/apps/dashboard/src/app/(dashboard)/published-contract/[publisher]/[contract_id]/components/module-list.client.tsx b/apps/dashboard/src/app/(dashboard)/published-contract/[publisher]/[contract_id]/components/module-list.client.tsx new file mode 100644 index 000000000..6adaa430e --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/published-contract/[publisher]/[contract_id]/components/module-list.client.tsx @@ -0,0 +1,36 @@ +"use client"; + +import { Badge } from "@/components/ui/badge"; +import { useSearchParams } from "next/navigation"; +import { useMemo } from "react"; +import { moduleFromBase64 } from "../../../utils/module-base-64"; + +export function useModules() { + const modules = useSearchParams()?.getAll("module") || []; + return useMemo( + () => modules.map(moduleFromBase64).filter((m) => m !== null), + [modules], + ); +} + +export function ModuleList() { + const modules = useModules(); + + // if nothing, render nothing + if (!modules.length) { + return null; + } + // else render stuff + return ( +
+ {modules.map((m) => ( + + {m.displayName} + + ))} +
+ ); +} diff --git a/apps/dashboard/src/app/(dashboard)/published-contract/[publisher]/[contract_id]/deploy/page.tsx b/apps/dashboard/src/app/(dashboard)/published-contract/[publisher]/[contract_id]/deploy/page.tsx new file mode 100644 index 000000000..bdb8cd3ae --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/published-contract/[publisher]/[contract_id]/deploy/page.tsx @@ -0,0 +1,25 @@ +import { setOverrides } from "lib/vercel-utils"; +import { DeployFormForPublishInfo } from "../../../components/publish-based-deploy"; +import { moduleFromBase64 } from "../../../utils/module-base-64"; + +setOverrides(); + +type Props = { + params: { + publisher: string; + contract_id: string; + }; + searchParams: { + module?: string[]; + }; +}; + +export default function PublishedContractDeployPage({ + params, + searchParams, +}: Props) { + const modules = searchParams.module + ?.map((m) => moduleFromBase64(m)) + .filter((m) => m !== null); + return ; +} diff --git a/apps/dashboard/src/app/(dashboard)/published-contract/[publisher]/[contract_id]/layout.tsx b/apps/dashboard/src/app/(dashboard)/published-contract/[publisher]/[contract_id]/layout.tsx new file mode 100644 index 000000000..256a46e05 --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/published-contract/[publisher]/[contract_id]/layout.tsx @@ -0,0 +1,35 @@ +import { PublishedContractBreadcrumbs } from "./components/breadcrumbs.client"; +import { getPublishedContractsWithPublisherMapping } from "./utils/getPublishedContractsWithPublisherMapping"; + +type Params = { publisher: string; contract_id: string }; + +export default function PublishedContractLayout(props: { + children: React.ReactNode; + params: Params; +}) { + return ( +
+ +
{props.children}
+
+ ); +} + +export async function generateMetadata({ params }: { params: Params }) { + const { publisher, contract_id } = params; + + const publishedContracts = await getPublishedContractsWithPublisherMapping({ + publisher: publisher, + contract_id: contract_id, + }); + + const publishedContract = publishedContracts[0]; + + const publishedContractName = + publishedContract?.displayName || publishedContract?.name; + + return { + title: `${publishedContractName} | Published Smart Contract`, + description: `${publishedContract.description}${publishedContract.description ? ". " : ""}Deploy ${publishedContractName} in one click with thirdweb.`, + }; +} diff --git a/apps/dashboard/src/app/(dashboard)/published-contract/[publisher]/[contract_id]/opengraph-image.tsx b/apps/dashboard/src/app/(dashboard)/published-contract/[publisher]/[contract_id]/opengraph-image.tsx new file mode 100644 index 000000000..b0c85f356 --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/published-contract/[publisher]/[contract_id]/opengraph-image.tsx @@ -0,0 +1,53 @@ +import { fetchPublisherProfile } from "components/contract-components/fetch-contracts-with-versions"; +import { format } from "date-fns/format"; +import { correctAndUniqueLicenses } from "lib/licenses"; +import { setOverrides } from "lib/vercel-utils"; +import { getPublishedContractsWithPublisherMapping } from "./utils/getPublishedContractsWithPublisherMapping"; +import { publishedContractOGImageTemplate } from "./utils/publishedContractOGImageTemplate"; + +export const runtime = "edge"; + +export const size = { + width: 1200, + height: 630, +}; + +export default async function Image(props: { + params: { + publisher: string; + contract_id: string; + }; +}) { + setOverrides(); + const { publisher, contract_id } = props.params; + + const [publishedContracts, publisherProfile] = await Promise.all([ + getPublishedContractsWithPublisherMapping({ + publisher: publisher, + contract_id: contract_id, + }), + fetchPublisherProfile(publisher), + ]); + + const publishedContract = publishedContracts[0]; + + const publishedContractName = + publishedContract?.displayName || publishedContract?.name; + + return publishedContractOGImageTemplate({ + title: publishedContractName, + description: publishedContract.description, + version: publishedContract.version || "latest", + publisher: publisherProfile?.name || publisher, + license: correctAndUniqueLicenses(publishedContract.licenses), + publishDate: format( + new Date( + Number.parseInt(publishedContract.publishTimestamp.toString() || "0") * + 1000, + ), + "MMM dd, yyyy", + ), + logo: publishedContract.logo, + publisherAvatar: publisherProfile?.avatar, + }); +} 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 new file mode 100644 index 000000000..d86721a57 --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/published-contract/[publisher]/[contract_id]/page.tsx @@ -0,0 +1,54 @@ +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"; +import { PublishedActions } from "../../components/contract-actions-published.client"; +import { DeployContractHeader } from "../../components/contract-header"; +import { getPublishedContractsWithPublisherMapping } from "./utils/getPublishedContractsWithPublisherMapping"; + +setOverrides(); + +type PublishedContractDeployPageProps = { + params: { + publisher: string; + contract_id: string; + }; +}; + +export default async function PublishedContractPage( + props: PublishedContractDeployPageProps, +) { + const publishedContractVersions = + await getPublishedContractsWithPublisherMapping({ + publisher: props.params.publisher, + contract_id: props.params.contract_id, + }); + + const publishedContract = publishedContractVersions[0]; + + return ( + <> + + + + + {/* TODO: remove the chakra things :) */} + + + + + + + ); +} diff --git a/apps/dashboard/src/app/(dashboard)/published-contract/[publisher]/[contract_id]/utils/getPublishedContractsWithPublisherMapping.ts b/apps/dashboard/src/app/(dashboard)/published-contract/[publisher]/[contract_id]/utils/getPublishedContractsWithPublisherMapping.ts new file mode 100644 index 000000000..ce13d2283 --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/published-contract/[publisher]/[contract_id]/utils/getPublishedContractsWithPublisherMapping.ts @@ -0,0 +1,34 @@ +import { thirdwebClient } from "@/constants/client"; +import { isAddress } from "thirdweb"; +import { resolveAddress } from "thirdweb/extensions/ens"; +import { fetchPublishedContractVersions } from "../../../../../../components/contract-components/fetch-contracts-with-versions"; + +function mapThirdwebPublisher(publisher: string) { + if (publisher === "thirdweb.eth") { + return "deployer.thirdweb.eth"; + } + return publisher; +} + +export async function getPublishedContractsWithPublisherMapping(options: { + publisher: string; + contract_id: string; +}) { + const { publisher, contract_id } = options; + + // resolve ENS + const publisherAddress = isAddress(publisher) + ? publisher + : await resolveAddress({ + client: thirdwebClient, + name: mapThirdwebPublisher(publisher), + }); + + // get all the published versions of the contract + const publishedContractVersions = await fetchPublishedContractVersions( + publisherAddress, + contract_id, + ); + + return publishedContractVersions; +} diff --git a/apps/dashboard/src/pages/api/og/publishedContract.tsx b/apps/dashboard/src/app/(dashboard)/published-contract/[publisher]/[contract_id]/utils/publishedContractOGImageTemplate.tsx similarity index 70% rename from apps/dashboard/src/pages/api/og/publishedContract.tsx rename to apps/dashboard/src/app/(dashboard)/published-contract/[publisher]/[contract_id]/utils/publishedContractOGImageTemplate.tsx index d53e387ad..c0755e366 100644 --- a/apps/dashboard/src/pages/api/og/publishedContract.tsx +++ b/apps/dashboard/src/app/(dashboard)/published-contract/[publisher]/[contract_id]/utils/publishedContractOGImageTemplate.tsx @@ -1,37 +1,7 @@ /* eslint-disable @next/next/no-img-element */ -import { isProd } from "@/constants/env"; -import { ImageResponse } from "@vercel/og"; -import type { NextRequest } from "next/server"; -import { PublishedContractOG } from "og-lib/url-utils"; - -// Make sure the font exists in the specified path: -export const config = { - runtime: "edge", -}; - -const image = fetch( - new URL("og-lib/assets/publish/background.png", import.meta.url), -).then((res) => res.arrayBuffer()); - -const inter400_ = fetch( - new URL("og-lib/fonts/inter/400.ttf", import.meta.url), -).then((res) => res.arrayBuffer()); -const inter500_ = fetch( - new URL("og-lib/fonts/inter/500.ttf", import.meta.url), -).then((res) => res.arrayBuffer()); -const inter700_ = fetch( - new URL("og-lib/fonts/inter/700.ttf", import.meta.url), -).then((res) => res.arrayBuffer()); - -const ibmPlexMono400_ = fetch( - new URL("og-lib/fonts/ibm-plex-mono/400.ttf", import.meta.url), -).then((res) => res.arrayBuffer()); -const ibmPlexMono500_ = fetch( - new URL("og-lib/fonts/ibm-plex-mono/500.ttf", import.meta.url), -).then((res) => res.arrayBuffer()); -const ibmPlexMono700_ = fetch( - new URL("og-lib/fonts/ibm-plex-mono/700.ttf", import.meta.url), -).then((res) => res.arrayBuffer()); +import { thirdwebClient } from "@/constants/client"; +import { ImageResponse } from "next/og"; +import { download } from "thirdweb/storage"; const OgBrandIcon: React.FC = () => ( // biome-ignore lint/a11y/noSvgWithoutTitle: not needed @@ -160,27 +130,49 @@ function descriptionShortener(description: string) { return `${words.join(" ")} ...`; } -const IPFS_GATEWAY = process.env.API_ROUTES_CLIENT_ID - ? `https://${process.env.API_ROUTES_CLIENT_ID}.${isProd ? "ipfscdn.io/ipfs/" : "thirdwebstorage-dev.com/ipfs/"}` - : "https://ipfs.io/ipfs/"; +export async function publishedContractOGImageTemplate(params: { + title: string; + description?: string; + version: string; + publisher: string; + license: string[]; + publishDate: string; + logo?: string; + publisherAvatar?: string; + extension?: string[]; +}) { + const image = fetch( + new URL("og-lib/assets/publish/background.png", import.meta.url), + ).then((res) => res.arrayBuffer()); -function replaceAnyIpfsUrlWithGateway(url: string) { - if (url.startsWith("ipfs://")) { - return `${IPFS_GATEWAY}${url.slice(7)}`; - } - if (url.includes("/ipfs/")) { - const [, after] = url.split("/ipfs/"); - return `${IPFS_GATEWAY}${after}`; - } - return url; -} + const inter400_ = fetch( + new URL("../../../../../../og-lib/fonts/inter/400.ttf", import.meta.url), + ).then((res) => res.arrayBuffer()); + const inter500_ = fetch( + new URL("../../../../../../og-lib/fonts/inter/500.ttf", import.meta.url), + ).then((res) => res.arrayBuffer()); + const inter700_ = fetch( + new URL("../../../../../../og-lib/fonts/inter/700.ttf", import.meta.url), + ).then((res) => res.arrayBuffer()); -export default async function handler(req: NextRequest) { - if (req.method !== "GET") { - return new Response("Method not allowed", { status: 405 }); - } - - const publishedContractData = PublishedContractOG.fromUrl(new URL(req.url)); + const ibmPlexMono400_ = fetch( + new URL( + "../../../../../../og-lib/fonts/ibm-plex-mono/400.ttf", + import.meta.url, + ), + ).then((res) => res.arrayBuffer()); + const ibmPlexMono500_ = fetch( + new URL( + "../../../../../../og-lib/fonts/ibm-plex-mono/500.ttf", + import.meta.url, + ), + ).then((res) => res.arrayBuffer()); + const ibmPlexMono700_ = fetch( + new URL( + "../../../../../../og-lib/fonts/ibm-plex-mono/700.ttf", + import.meta.url, + ), + ).then((res) => res.arrayBuffer()); const [ inter400, @@ -190,6 +182,8 @@ export default async function handler(req: NextRequest) { ibmPlexMono500, ibmPlexMono700, imageData, + logo, + avatar, ] = await Promise.all([ inter400_, inter500_, @@ -198,6 +192,18 @@ export default async function handler(req: NextRequest) { ibmPlexMono500_, ibmPlexMono700_, image, + params.logo + ? download({ + uri: params.logo, + client: thirdwebClient, + }).then((res) => res.arrayBuffer()) + : undefined, + params.publisherAvatar + ? download({ + uri: params.publisherAvatar, + client: thirdwebClient, + }).then((res) => res.arrayBuffer()) + : undefined, ]); return new ImageResponse( @@ -223,37 +229,30 @@ export default async function handler(req: NextRequest) {
- {publishedContractData.logo && ( - - )} -

- {publishedContractData.name} -

+ {/* @ts-expect-error - this works fine */} + {logo && } +

{params.title}

- {publishedContractData?.description && ( + {params?.description && (

- {descriptionShortener(publishedContractData.description)} + {descriptionShortener(params.description)}

)}
- + {avatar && ( + + )} +

- {publishedContractData.publisher} + {params.publisher}

@@ -264,15 +263,15 @@ export default async function handler(req: NextRequest) { fontFamily: "IBM Plex Mono", }} > - {publishedContractData.extension?.length ? ( + {params.extension?.length ? (
  • - {Array.isArray(publishedContractData.extension) - ? categorizeExtensions(publishedContractData.extension).map( + {Array.isArray(params.extension) + ? categorizeExtensions(params.extension).map( ([ext, count]) => ( {ext} @@ -282,27 +281,27 @@ export default async function handler(req: NextRequest) { ), ) - : publishedContractData.extension} + : params.extension}
  • ) : null} - {publishedContractData.license?.length ? ( + {params.license?.length ? (
  • - {Array.isArray(publishedContractData.license) - ? publishedContractData.license.join(", ") - : publishedContractData.license} + {Array.isArray(params.license) + ? params.license.join(", ") + : params.license}
  • ) : null}
  • - {publishedContractData.version} + {params.version}
  • - {publishedContractData.publishDate} + {params.publishDate}
  • diff --git a/apps/dashboard/src/app/(dashboard)/published-contract/components/contract-actions-deploy.client.tsx b/apps/dashboard/src/app/(dashboard)/published-contract/components/contract-actions-deploy.client.tsx new file mode 100644 index 000000000..3d073b2b0 --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/published-contract/components/contract-actions-deploy.client.tsx @@ -0,0 +1,41 @@ +"use client"; + +import { Button } from "@/components/ui/button"; +import { shareLink } from "@/lib/shareLink"; +import Link from "next/link"; +import { useSearchParams } from "next/navigation"; + +export function DeployActions(props: { + publisher: string; + contract_id: string; + version?: string; + dispayName: string; +}) { + const searchparams = useSearchParams(); + + const stringifiedSearchParams = searchparams?.toString(); + + return ( +
    + + + +
    + ); +} diff --git a/apps/dashboard/src/app/(dashboard)/published-contract/components/contract-actions-published.client.tsx b/apps/dashboard/src/app/(dashboard)/published-contract/components/contract-actions-published.client.tsx new file mode 100644 index 000000000..13efeebc2 --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/published-contract/components/contract-actions-published.client.tsx @@ -0,0 +1,40 @@ +"use client"; + +import { Button } from "@/components/ui/button"; +import { shareLink } from "@/lib/shareLink"; +import { ChevronsRightIcon } from "lucide-react"; +import Link from "next/link"; +import { useSearchParams } from "next/navigation"; + +export function PublishedActions(props: { + publisher: string; + contract_id: string; + version?: string; + dispayName: string; +}) { + const searchparams = useSearchParams(); + const stringifiedSearchParams = searchparams?.toString(); + + return ( +
    + + +
    + ); +} diff --git a/apps/dashboard/src/app/(dashboard)/published-contract/components/contract-header.tsx b/apps/dashboard/src/app/(dashboard)/published-contract/components/contract-header.tsx new file mode 100644 index 000000000..1f4bdef4e --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/published-contract/components/contract-header.tsx @@ -0,0 +1,45 @@ +"use client"; +import type { PropsWithChildren } from "react"; +import type { PublishedContractWithVersion } from "../../../../components/contract-components/fetch-contracts-with-versions"; +import { ModuleList } from "../[publisher]/[contract_id]/components/module-list.client"; +import { DeployContractInfo } from "./contract-info"; +import { DeployContractVersionSelector } from "./version-selector"; + +type DeployContractHeaderProps = { + publisher: string; + contract_id: string; + version?: string; + allVersions: PublishedContractWithVersion[]; + activeVersion: PublishedContractWithVersion; +}; + +export function DeployContractHeader( + props: PropsWithChildren, +) { + return ( +
    +
    + +
    + { + v.version) + .filter((v) => v !== undefined) || [] + } + version={props.activeVersion.version || "latest"} + /> + } + {props.children} +
    +
    + +
    + ); +} diff --git a/apps/dashboard/src/app/(dashboard)/published-contract/components/contract-info.tsx b/apps/dashboard/src/app/(dashboard)/published-contract/components/contract-info.tsx new file mode 100644 index 000000000..314dbb97b --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/published-contract/components/contract-info.tsx @@ -0,0 +1,38 @@ +import { thirdwebClient } from "@/constants/client"; +import { resolveScheme } from "thirdweb/storage"; + +export function DeployContractInfo(props: { + name: string; + displayName?: string; + description?: string; + logo?: string; +}) { + const contractNameDisplay = props.displayName || props.name; + + return ( +
    +
    + {props.logo && ( +
    + {/*eslint-disable-next-line @next/next/no-img-element*/} + {props.name} +
    + )} + +
    +

    + {contractNameDisplay} +

    +

    {props.description}

    +
    +
    +
    + ); +} diff --git a/apps/dashboard/src/app/(dashboard)/published-contract/components/publish-based-deploy.tsx b/apps/dashboard/src/app/(dashboard)/published-contract/components/publish-based-deploy.tsx new file mode 100644 index 000000000..6c3a8cb17 --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/published-contract/components/publish-based-deploy.tsx @@ -0,0 +1,89 @@ +import { Separator } from "@/components/ui/separator"; +import { thirdwebClient } from "@/constants/client"; +import { isAddress } from "thirdweb"; +import { fetchDeployMetadata } from "thirdweb/contract"; +import { resolveAddress } from "thirdweb/extensions/ens"; +import { + fetchPublishedContractVersion, + fetchPublishedContractVersions, +} from "../../../../components/contract-components/fetch-contracts-with-versions"; +import { DeployActions } from "./contract-actions-deploy.client"; +import { DeployContractHeader } from "./contract-header"; +import { DeployFormForUri } from "./uri-based-deploy"; + +type PublishBasedDeployProps = { + publisher: string; + contract_id: string; + version?: string; + modules?: Array<{ publisher: string; moduleId: string; version?: string }>; +}; + +function mapThirdwebPublisher(publisher: string) { + if (publisher === "thirdweb.eth") { + return "deployer.thirdweb.eth"; + } + return publisher; +} + +export async function DeployFormForPublishInfo(props: PublishBasedDeployProps) { + // resolve ENS if required + const publisherAddress = isAddress(props.publisher) + ? props.publisher + : await resolveAddress({ + client: thirdwebClient, + name: mapThirdwebPublisher(props.publisher), + }); + + // get all the published versions of the contract + const [publishedContractVersions, ...modules] = await Promise.all([ + fetchPublishedContractVersions(publisherAddress, props.contract_id), + ...(props.modules || []).map((m) => + fetchPublishedContractVersion(m.publisher, m.moduleId, m.version), + ), + ]); + + // determine the "active" version of the contract based on the version that is passed + const publishedContract = + publishedContractVersions.find((v) => v.version === props.version) || + publishedContractVersions[0]; + + const moduleUris = modules + .filter((m) => m !== null) + .map((m) => m.publishMetadataUri); + const [contractMetadata, ...fetchedModules] = await Promise.all([ + fetchDeployMetadata({ + client: thirdwebClient, + // force `ipfs://` prefix + uri: publishedContract.publishMetadataUri.startsWith("ipfs://") + ? publishedContract.publishMetadataUri + : `ipfs://${publishedContract.publishMetadataUri}`, + }).catch(() => null), + ...(moduleUris || []).map((uri) => + fetchDeployMetadata({ + client: thirdwebClient, + // force `ipfs://` prefix + uri: uri.startsWith("ipfs://") ? uri : `ipfs://${uri}`, + }).catch(() => null), + ), + ]); + + return ( + <> + + + + + m !== null)} + /> + + ); +} diff --git a/apps/dashboard/src/app/(dashboard)/published-contract/components/uri-based-deploy.tsx b/apps/dashboard/src/app/(dashboard)/published-contract/components/uri-based-deploy.tsx new file mode 100644 index 000000000..a0a1f780a --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/published-contract/components/uri-based-deploy.tsx @@ -0,0 +1,26 @@ +import { ChakraProviderSetup } from "@/components/ChakraProviderSetup"; +import { CustomContractForm } from "components/contract-components/contract-deploy-form/custom-contract"; +import type { FetchDeployMetadataResult } from "thirdweb/contract"; + +type DeployFormForUriProps = { + contractMetadata: FetchDeployMetadataResult | null; + modules: FetchDeployMetadataResult[] | null; +}; + +export async function DeployFormForUri(props: DeployFormForUriProps) { + const { contractMetadata, modules } = props; + + if (!contractMetadata) { + return
    Could not fetch metadata
    ; + } + + // TODO: remove the `ChakraProviderSetup` wrapper once the form is updated to no longer use chrakra + return ( + + m !== null)} + /> + + ); +} diff --git a/apps/dashboard/src/app/(dashboard)/published-contract/components/version-selector.tsx b/apps/dashboard/src/app/(dashboard)/published-contract/components/version-selector.tsx new file mode 100644 index 000000000..5708b486f --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/published-contract/components/version-selector.tsx @@ -0,0 +1,76 @@ +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { useDashboardRouter } from "@/lib/DashboardRouter"; +import { usePathname, useSearchParams } from "next/navigation"; +import { useMemo } from "react"; + +type DeployContractVersionSelectorProps = { + availableVersions: string[]; + version: string; +}; + +export function DeployContractVersionSelector({ + availableVersions, + version, +}: DeployContractVersionSelectorProps) { + const router = useDashboardRouter(); + const pathname = usePathname() || ""; + const searchParams = useSearchParams(); + + const { isDeploy, rootPathName } = useMemo(() => { + return { + isDeploy: pathname.endsWith("/deploy"), + rootPathName: pathname + .replace(/\/deploy$/, "") + .replace(`/${version}`, ""), + }; + }, [pathname, version]); + + return ( + + ); +} diff --git a/apps/dashboard/src/app/(dashboard)/published-contract/page.tsx b/apps/dashboard/src/app/(dashboard)/published-contract/page.tsx new file mode 100644 index 000000000..ae2afa2d9 --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/published-contract/page.tsx @@ -0,0 +1,28 @@ +import { thirdwebClient } from "@/constants/client"; +import { redirect } from "next/navigation"; +import { fetchDeployMetadata } from "thirdweb/contract"; + +export default async function Page(props: { + searchParams: { + uri?: string; + }; +}) { + if (!props.searchParams?.uri) { + // redirect back out if we do not have a uri + return redirect("/dashboard"); + } + + const contractMetadata = await fetchDeployMetadata({ + client: thirdwebClient, + // force `ipfs://` prefix + uri: props.searchParams.uri.startsWith("ipfs://") + ? props.searchParams.uri + : `ipfs://${props.searchParams.uri}`, + }).catch(() => null); + + return ( +
    +
    {JSON.stringify(contractMetadata, null, 2)}
    +
    + ); +} diff --git a/apps/dashboard/src/app/(dashboard)/published-contract/utils/module-base-64.ts b/apps/dashboard/src/app/(dashboard)/published-contract/utils/module-base-64.ts new file mode 100644 index 000000000..46dbc7435 --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/published-contract/utils/module-base-64.ts @@ -0,0 +1,36 @@ +type Module = { + publisher: string; + moduleId: string; + version?: string; +}; + +function assertModule(val: unknown): asserts val is Module { + if ( + typeof val !== "object" || + val === null || + typeof (val as Module).publisher !== "string" || + typeof (val as Module).moduleId !== "string" || + ((val as Module).version !== undefined && + typeof (val as Module).version !== "string") + ) { + throw new Error("Invalid module"); + } +} + +export function moduleFromBase64(base64: string) { + try { + const mod = JSON.parse(atob(base64)); + assertModule(mod); + return { + ...mod, + version: mod.version || "latest", + displayName: mod.moduleId.split("ERC")[0], + }; + } catch { + return null; + } +} + +export function moduleToBase64(mod: Module) { + return btoa(JSON.stringify(mod)); +} diff --git a/apps/dashboard/src/app/(dashboard)/support/components/product.ts b/apps/dashboard/src/app/(dashboard)/support/components/product.ts deleted file mode 100644 index 31069b5b2..000000000 --- a/apps/dashboard/src/app/(dashboard)/support/components/product.ts +++ /dev/null @@ -1,12 +0,0 @@ -export const PRODUCTS = [ - "connect", - "engine", - "contracts", - "account", - "other", -] as const; -export type Product = (typeof PRODUCTS)[number]; - -export function isProduct(value: string): value is Product { - return PRODUCTS.includes(value as Product); -} diff --git a/apps/dashboard/src/components/analytics/auto-bar-chart.tsx b/apps/dashboard/src/components/analytics/auto-bar-chart.tsx deleted file mode 100644 index f158d7ebe..000000000 --- a/apps/dashboard/src/components/analytics/auto-bar-chart.tsx +++ /dev/null @@ -1,247 +0,0 @@ -import { useTheme } from "next-themes"; -import { useId, useMemo, useState } from "react"; -import { - Bar, - BarChart as RechartsBarChart, - ResponsiveContainer, - Tooltip, - XAxis, - YAxis, -} from "recharts"; -import { StackToolTip } from "./stack-tooltip"; - -type GenericDataType = Record; - -type IndexType = "date"; - -interface AutoBarChartProps< - TData extends GenericDataType, - TIndexKey extends keyof TData, -> { - data: TData[]; - index: { - id: TIndexKey; - label?: string; - type?: IndexType; - format?: (index: TData[TIndexKey]) => string; - }; - showXAxis?: boolean; - showYAxis?: boolean; - stacked?: boolean; -} - -const BAR_COLORS_LIGHT = [ - "#649CDD", - "#92BBE8", - "#407DCC", - "#94BD5D", - "#729544", - "#A48AC1", - "#C5B5D7", - "#8460AB", - "#E1918E", - "#D65E59", - "#F3D570", - "#F7E5A2", - "#EFC64A", - "#E7AB78", - "#F0CDAD", - "#DF8A47", - "#A6D7D8", - "#CEE9E9", -]; - -const BAR_COLORS_DARK = [ - "#3682DA", - "#6AADF5", - "#1769D3", - "#84BF35", - "#629421", - "#8751C3", - "#AA85D3", - "#6E38A9", - "#DE6F6B", - "#D43932", - "#FACD38", - "#FFE689", - "#F0BF2A", - "#E4934E", - "#F4C294", - "#DE7929", - "#7CDEE0", - "#C8FFFF", -]; - -export const AutoBarChart = < - TData extends GenericDataType, - TIndexKey extends keyof TData, ->({ - data, - index, - showXAxis, - showYAxis, - stacked, -}: AutoBarChartProps) => { - const { theme } = useTheme(); - const [hoverKey, setHoverKey] = useState(""); - const id = useId(); - - const barColors = useMemo(() => { - if (theme === "light") { - return BAR_COLORS_LIGHT; - } - - return BAR_COLORS_DARK; - }, [theme]); - - const categories = useMemo(() => { - const autoKeys: string[] = []; - // biome-ignore lint/complexity/noForEach: FIXME - data.forEach((item) => { - for (const key of Object.keys(item)) { - if (key === index.id) { - continue; - } - - if (!autoKeys.includes(key)) { - autoKeys.push(key); - } - } - }); - - return autoKeys.map((key, idx) => ({ - id: key, - label: key, - color: barColors[idx % barColors.length], - })); - }, [data, index.id, barColors]); - - const sortedData = useMemo(() => { - return [...data].sort( - (a, b) => (a[index.id] as number) - (b[index.id] as number), - ); - }, [data, index]); - - if (!data.length) { - return null; - } - - return ( - - - - {categories.map((cat) => ( - - - - - ))} - - - {categories.map((cat) => ( - { - setHoverKey(item.tooltipPayload[0].dataKey); - }} - onMouseLeave={() => { - setHoverKey(""); - }} - /> - ))} - { - if (!active || !payload) { - return null; - } - - const { time, ...values } = payload[0].payload; - return ( - - ); - }} - cursor={ - !hoverKey || categories.length <= 1 - ? { - stroke: "#EAEAEA", - fill: "#EAEAEA", - opacity: 0.3, - strokeDasharray: 2, - strokeWidth: 0, - } - : false - } - /> - - - index.format - ? index.format(payload) - : index.type === "date" - ? new Date(payload).toLocaleDateString(undefined, { - day: "2-digit", - month: "2-digit", - year: "2-digit", - }) - : payload - } - style={{ - fontSize: "12px", - fontFamily: "var(--chakra-fonts-body)", - }} - stroke="var(--chakra-colors-paragraph)" - tickLine={false} - axisLine={false} - interval="preserveStartEnd" - minTickGap={5} - domain={["dataMin - 86400000", "dataMax + 86400000"]} - type="number" - tick={{ transform: "translate(0, 6)" }} - ticks={[ - sortedData[0][index.id], - sortedData[Math.floor(sortedData.length / 2)][index.id], - sortedData[sortedData.length - 1][index.id], - ]} - /> - - { - return payload.toLocaleString(); - }} - className="text-xs font-sans" - type="number" - stroke="hsl(var(--muted-foreground))" - tickLine={false} - axisLine={false} - /> - - - ); -}; diff --git a/apps/dashboard/src/components/analytics/chart-container.tsx b/apps/dashboard/src/components/analytics/chart-container.tsx deleted file mode 100644 index 419df2b30..000000000 --- a/apps/dashboard/src/components/analytics/chart-container.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { AspectRatio } from "@/components/ui/aspect-ratio"; -import { cn } from "@/lib/utils"; -import { Suspense } from "react"; -import type { ComponentWithChildren } from "types/component-with-children"; -import { AreaChartLoadingState } from "./area-chart"; - -type SupportedChartType = "area"; - -interface ChartContainerProps { - chartTitle?: JSX.Element; - chartType?: SupportedChartType; - ratio: number; - className?: string; -} - -export const ChartContainer: ComponentWithChildren = ({ - chartType = "area", - chartTitle, - ratio, - children, - className, -}) => { - return ( -
    - {chartTitle} - - {/* suspense to handle loading state */} - }> - {children} - - -
    - ); -}; - -const ChartLoadingState: React.FC> = ({ - chartType, -}) => { - if (chartType === "area") { - return ; - } - return null; -}; diff --git a/apps/dashboard/src/components/analytics/stack-tooltip.tsx b/apps/dashboard/src/components/analytics/stack-tooltip.tsx deleted file mode 100644 index 1d3de398c..000000000 --- a/apps/dashboard/src/components/analytics/stack-tooltip.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { cn } from "@/lib/utils"; - -type CustomToolTipProps = { - time: string; - hoverKey: string; - values: Record; -}; - -const formattingOptions: Intl.DateTimeFormatOptions = { - year: "numeric", - month: "short", - day: "numeric", -}; - -export const StackToolTip: React.FC = ({ - time, - values, - hoverKey, -}) => { - return ( -
    -
    - -

    - {new Date(time).toLocaleDateString(undefined, formattingOptions)} -

    -
    - -
    - {Object.entries(values) - .reverse() - .map(([key, value]) => ( -

    - {hoverKey && key !== hoverKey ? ( - `${key}: ` - ) : ( - {key}: - )} - {value.toLocaleString()} -

    - ))} -
    -
    - ); -}; diff --git a/apps/dashboard/src/components/contract-components/contract-deploy-form/contract-metadata-fieldset.tsx b/apps/dashboard/src/components/contract-components/contract-deploy-form/contract-metadata-fieldset.tsx index cbceedcb3..3a9d5b6b2 100644 --- a/apps/dashboard/src/components/contract-components/contract-deploy-form/contract-metadata-fieldset.tsx +++ b/apps/dashboard/src/components/contract-components/contract-deploy-form/contract-metadata-fieldset.tsx @@ -3,24 +3,21 @@ import { FormControl, Input } from "@chakra-ui/react"; import { FileInput } from "components/shared/FileInput"; import { useImageFileOrUrl } from "hooks/useImageFileOrUrl"; import { FormErrorMessage, FormLabel } from "tw-components"; -import type { useContractPublishMetadataFromURI } from "../hooks"; import { Fieldset } from "./common"; import type { CustomContractDeploymentForm } from "./custom-contract"; interface ContractMetadataFieldsetProps { form: CustomContractDeploymentForm; - metadata: ReturnType; } export const ContractMetadataFieldset: React.FC< ContractMetadataFieldsetProps -> = ({ form, metadata }) => { +> = ({ form }) => { return (
    void; - isImplementationDeploy?: true; - onSuccessCallback?: (contractAddress: string) => void; - walletAddress: string | undefined; + metadata: FetchDeployMetadataResult; + modules?: FetchDeployMetadataResult[]; } export type CustomContractDeploymentFormData = { @@ -82,12 +69,7 @@ export type CustomContractDeploymentFormData = { saltForCreate2: string; signerAsSalt: boolean; deployParams: Record; - defaultModules: { - moduleName: string; - moduleVersion: string; - publisherAddress: string; - }[]; - modularContractDefaultModulesInstallParams: Record[]; + moduleData: Record>; contractMetadata?: { name: string; description: string; @@ -115,32 +97,35 @@ const voteParamsSet = new Set([ "_initialVoteQuorumFraction", ]); -const CustomContractForm: React.FC = ({ - ipfsHash, - version, - selectedChain, - onChainSelect, - isImplementationDeploy, - onSuccessCallback, - walletAddress, -}) => { - const { data: transactions } = useTransactionsForDeploy(ipfsHash); - const networkInfo = useSupportedChain(selectedChain || -1); - const ensQuery = useEns(walletAddress); - const connectedWallet = ensQuery.data?.address || walletAddress; - const trackEvent = useTrack(); - const activeAccount = useActiveAccount(); - const searchParams = useSearchParams(); - const chain = useV5DashboardChain(selectedChain); +function checkTwPublisher(publisher: string | undefined) { + switch (publisher) { + case "deployer.thirdweb.eth": + case "thirdweb.eth": + // the literal publisher address + case "0xdd99b75f095d0c4d5112aCe938e4e6ed962fb024": + return true; + default: + return false; + } +} + +export const CustomContractForm: React.FC = ({ + metadata, + modules, +}) => { + const activeAccount = useActiveAccount(); + const walletChain = useActiveWalletChain(); + useLoggedInUser(); + const { onError } = useTxNotifications( + "Successfully deployed contract", + "Failed to deploy contract", + ); - const compilerMetadata = useContractPublishMetadataFromURI(ipfsHash); - const fullPublishMetadata = useContractFullPublishMetadata(ipfsHash); const constructorParams = - compilerMetadata.data?.abi?.find((c) => c.type === "constructor")?.inputs || - []; + metadata.abi.find((a) => a.type === "constructor")?.inputs || []; const [customFactoryNetwork, customFactoryAddress] = Object.entries( - fullPublishMetadata.data?.factoryDeploymentData?.customFactoryInput + metadata?.factoryDeploymentData?.customFactoryInput ?.customFactoryAddresses || {}, )[0] || ["", ""]; @@ -149,50 +134,33 @@ const CustomContractForm: React.FC = ({ Number(customFactoryNetwork), ); - const isTWPublisher = - fullPublishMetadata.data?.publisher === "deployer.thirdweb.eth"; + const isTWPublisher = checkTwPublisher(metadata?.publisher); const initializerParams = useFunctionParamsFromABI( - (fullPublishMetadata.data?.deployType === "customFactory" && - customFactoryAbi?.data + metadata?.deployType === "customFactory" && customFactoryAbi?.data ? customFactoryAbi.data - : compilerMetadata.data?.abi) as Abi | undefined, - fullPublishMetadata.data?.deployType === "customFactory" - ? fullPublishMetadata.data?.factoryDeploymentData?.customFactoryInput - ?.factoryFunction || "deployProxyByImplementation" - : fullPublishMetadata.data?.factoryDeploymentData - ?.implementationInitializerFunction || "initialize", + : metadata?.abi, + metadata?.deployType === "customFactory" + ? metadata?.factoryDeploymentData?.customFactoryInput?.factoryFunction || + "deployProxyByImplementation" + : metadata?.factoryDeploymentData?.implementationInitializerFunction || + "initialize", ); const isFactoryDeployment = - ((fullPublishMetadata.data?.isDeployableViaFactory || - fullPublishMetadata.data?.isDeployableViaProxy) && - !isImplementationDeploy) || - fullPublishMetadata.data?.deployType === "autoFactory" || - fullPublishMetadata.data?.deployType === "customFactory"; + metadata?.isDeployableViaFactory || + metadata?.isDeployableViaProxy || + metadata?.deployType === "autoFactory" || + metadata?.deployType === "customFactory"; - const isModular = fullPublishMetadata.data?.routerType === "modular"; - - const defaultModules = (searchParams?.getAll("module") || []) - .map((m) => JSON.parse(atob(m))) - .map((m) => ({ - publisherAddress: m.publisher, - moduleName: m.moduleId, - moduleVersion: m.version || "latest", - })); - - const modularContractDefaultModulesInstallParams = - useModularContractsDefaultModulesInstallParams({ - defaultModules, - isQueryEnabled: isModular, - }); + const isModular = metadata?.routerType === "modular"; const deployParams = (isFactoryDeployment ? initializerParams : constructorParams) || []; const isAccountFactory = !isFactoryDeployment && - (fullPublishMetadata.data?.name.includes("AccountFactory") || false); + (metadata?.name.includes("AccountFactory") || false); const parsedDeployParams = useMemo( () => ({ @@ -203,17 +171,15 @@ const CustomContractForm: React.FC = ({ } acc[param.name] = replaceTemplateValues( - fullPublishMetadata.data?.constructorParams?.[param.name] - ?.defaultValue - ? fullPublishMetadata.data?.constructorParams?.[param.name] - ?.defaultValue || "" + metadata?.constructorParams?.[param.name]?.defaultValue + ? metadata?.constructorParams?.[param.name]?.defaultValue || "" : param.name === "_royaltyBps" || param.name === "_platformFeeBps" ? "0" : "", param.type, { - connectedWallet, - chainId: selectedChain, + connectedWallet: activeAccount?.address, + chainId: walletChain?.id, }, ); return acc; @@ -221,55 +187,57 @@ const CustomContractForm: React.FC = ({ {} as Record, ), }), - [ - deployParams, - fullPublishMetadata.data?.constructorParams, - connectedWallet, - selectedChain, - ], + [deployParams, metadata?.constructorParams, activeAccount, walletChain?.id], ); const transformedQueryData = useMemo( - () => ({ - addToDashboard: true, - deployDeterministic: isAccountFactory, - saltForCreate2: "", - signerAsSalt: true, - deployParams: parsedDeployParams, - recipients: [{ address: connectedWallet || "", sharesBps: 10000 }], - defaultModules, - // set default values for modular contract modules with custom components - modularContractDefaultModulesInstallParams: - (activeAccount && - isTWPublisher && - modularContractDefaultModulesInstallParams.data?.map((ext) => { - const paramNames = ext.params.map((param) => param.name); - const returnVal: Record = {}; + () => + ({ + addToDashboard: true, + deployDeterministic: isAccountFactory, + saltForCreate2: "", + signerAsSalt: true, + deployParams: parsedDeployParams, + recipients: [ + { address: activeAccount?.address || "", sharesBps: 10000 }, + ], + // set default values for modular contract modules with custom components + moduleData: + (activeAccount && + isTWPublisher && + modules?.reduce( + (acc, mod) => { + const params = getModuleInstallParams(mod); + const paramNames = params + .map((param) => param.name) + .filter((p) => p !== undefined); + const returnVal: Record = {}; - // set connected wallet address as default "royaltyRecipient" - if (showRoyaltyFieldset(paramNames)) { - returnVal.royaltyRecipient = activeAccount.address; - returnVal.royaltyBps = "0"; - returnVal.transferValidator = ZERO_ADDRESS; - } + // set connected wallet address as default "royaltyRecipient" + if (showRoyaltyFieldset(paramNames)) { + returnVal.royaltyRecipient = activeAccount.address; + returnVal.royaltyBps = "0"; + returnVal.transferValidator = ZERO_ADDRESS; + } - // set connected wallet address as default "primarySaleRecipient" - else if (showPrimarySaleFiedset(paramNames)) { - returnVal.primarySaleRecipient = activeAccount.address; - } + // set connected wallet address as default "primarySaleRecipient" + else if (showPrimarySaleFiedset(paramNames)) { + returnVal.primarySaleRecipient = activeAccount.address; + } - return returnVal; - })) || - [], - }), + acc[mod.name] = returnVal; + return acc; + }, + {} as Record>, + )) || + {}, + }) satisfies CustomContractDeploymentFormData, [ parsedDeployParams, isAccountFactory, - connectedWallet, - modularContractDefaultModulesInstallParams.data, - isTWPublisher, activeAccount, - defaultModules, + modules, + isTWPublisher, ], ); @@ -294,12 +262,6 @@ const CustomContractForm: React.FC = ({ "_platformFeeRecipient" in formDeployParams; const isSplit = "_payees" in formDeployParams && "_shares" in formDeployParams; - const isVote = - "_initialVotingDelay" in formDeployParams && - "_initialVotingPeriod" in formDeployParams && - "_initialProposalThreshold" in formDeployParams && - "_initialVoteQuorumFraction" in formDeployParams && - "_token" in formDeployParams; const hasTrustedForwarders = "_trustedForwarders" in formDeployParams; const rewardDeployParams = Object.keys(formDeployParams).filter((paramKey) => @@ -313,608 +275,579 @@ const CustomContractForm: React.FC = ({ const showRewardsSection = rewardDeployParams.length === rewardParamsSet.size; const showVoteSection = voteDeployParams.length === voteParamsSet.size; - const shouldHide = (paramKey: string) => { - if (isAccountFactory) { - return false; - } - if ( - (hasContractURI && - (paramKey === "_contractURI" || - paramKey === "_name" || - paramKey === "_symbol")) || - (hasRoyalty && - (paramKey === "_royaltyBps" || paramKey === "_royaltyRecipient")) || - (hasPrimarySale && paramKey === "_saleRecipient") || - (hasPrimarySaleRecipient && paramKey === "_primarySaleRecipient") || - (showRewardsSection && rewardParamsSet.has(paramKey)) || - (showVoteSection && voteParamsSet.has(paramKey)) || - (hasPlatformFee && - (paramKey === "_platformFeeBps" || - paramKey === "_platformFeeRecipient")) || - paramKey === "_defaultAdmin" || - (isSplit && (paramKey === "_payees" || paramKey === "_shares")) || - paramKey === "_trustedForwarders" - ) { - return true; - } - - return false; - }; - - const extensions = useContractEnabledExtensions( - compilerMetadata.data?.abi as Abi | undefined, - ); - const isErc721SharedMetadadata = extensions.some( - (extension) => extension.name === "ERC721SharedMetadata", - ); - - const deployStatusModal = useDeployStatusModal(); - const deploy = useCustomContractDeployMutation({ - ipfsHash, - version, - forceDirectDeploy: isImplementationDeploy, - hasContractURI, - hasRoyalty, - isSplit, - isVote, - isErc721SharedMetadadata, - deployStatusModal, - }); - - const router = useRouter(); - const { onSuccess, onError } = useTxNotifications( - "Successfully deployed contract", - "Failed to deploy contract", - ); - - const transactionCount = - (transactions?.length || 0) + - (form.watch("addToDashboard") ? 1 : 0) + - (isErc721SharedMetadadata ? 1 : 0); - - const isCreate2Deployment = form.watch("deployDeterministic"); - const advancedParams = Object.keys(formDeployParams) - .map((paramKey) => { - const deployParam = deployParams.find((p) => p.name === paramKey); - const contructorParams = - fullPublishMetadata.data?.constructorParams || {}; - const extraMetadataParam = contructorParams[paramKey]; - - if (shouldHide(paramKey) || !extraMetadataParam?.hidden) { - return null; + const shouldHide = useCallback( + (paramKey: string) => { + if (isAccountFactory) { + return false; + } + if ( + (hasContractURI && + (paramKey === "_contractURI" || + paramKey === "_name" || + paramKey === "_symbol")) || + (hasRoyalty && + (paramKey === "_royaltyBps" || paramKey === "_royaltyRecipient")) || + (hasPrimarySale && paramKey === "_saleRecipient") || + (hasPrimarySaleRecipient && paramKey === "_primarySaleRecipient") || + (showRewardsSection && rewardParamsSet.has(paramKey)) || + (showVoteSection && voteParamsSet.has(paramKey)) || + (hasPlatformFee && + (paramKey === "_platformFeeBps" || + paramKey === "_platformFeeRecipient")) || + paramKey === "_defaultAdmin" || + (isSplit && (paramKey === "_payees" || paramKey === "_shares")) || + paramKey === "_trustedForwarders" || + // hide module keys if we have `modules` passed + (modules?.length && + (paramKey === "_modules" || paramKey === "_moduleInstallData")) + ) { + return true; } - return ( - - ); - }) - .filter((x) => x !== null); + return false; + }, + [ + hasContractURI, + hasRoyalty, + hasPlatformFee, + hasPrimarySale, + hasPrimarySaleRecipient, + isSplit, + showRewardsSection, + showVoteSection, + isAccountFactory, + modules, + ], + ); + + const isCreate2Deployment = form.watch("deployDeterministic"); + const advancedParams = useMemo( + () => + Object.keys(formDeployParams) + .map((paramKey) => { + const deployParam = deployParams.find((p) => p.name === paramKey); + const contructorParams = metadata?.constructorParams || {}; + const extraMetadataParam = contructorParams[paramKey]; + + if (shouldHide(paramKey) || !extraMetadataParam?.hidden) { + return null; + } + + return ( + + ); + }) + .filter((x) => x !== null), + [formDeployParams, deployParams, metadata.constructorParams, shouldHide], + ); + + // mutations go here + + const deployStatusModal = useDeployStatusModal(); + + const deployMutation = useMutation({ + mutationFn: async (params: CustomContractDeploymentFormData) => { + if (!activeAccount) { + throw new Error("no account"); + } + if (!walletChain) { + throw new Error("no chain"); + } + + let _contractURI = ""; + + if (hasContractURI && params.contractMetadata) { + // upload the contract metadata + _contractURI = await upload({ + client: thirdwebClient, + files: [params.contractMetadata], + }); + } + + if (metadata.name === "MarketplaceV3") { + // special case for marketplace + return await deployMarketplaceContract({ + account: activeAccount, + chain: walletChain, + client: thirdwebClient, + params: { + name: params.contractMetadata?.name || "", + contractURI: _contractURI, + defaultAdmin: params.deployParams._defaultAdmin, + platformFeeBps: Number(params.deployParams._platformFeeBps), + platformFeeRecipient: params.deployParams._platformFeeRecipient, + trustedForwarders: JSON.parse( + params.deployParams._trustedForwarders, + ), + }, + }); + } + + const initializeParams = { + ...params.contractMetadata, + ...params.deployParams, + _contractURI, + }; + + const salt = params.deployDeterministic + ? params.signerAsSalt + ? activeAccount.address.concat(params.saltForCreate2) + : params.saltForCreate2 + : undefined; + + return await deployContractfromDeployMetadata({ + account: activeAccount, + chain: walletChain, + client: thirdwebClient, + deployMetadata: metadata, + initializeParams, + salt, + modules: modules?.map((m) => ({ + deployMetadata: m, + initializeParams: params.moduleData[m.name], + })), + }); + }, + }); + + const deployTransactions = useQuery({ + queryKey: [ + "deployTransactions", + { + chainId: walletChain?.id, + name: metadata.name, + publisher: metadata.publisher, + uri: metadata.metadataUri, + moduleUris: modules?.map((m) => m.metadataUri), + }, + ], + queryFn: async () => { + if (!walletChain) { + throw new Error("no wallet chain"); + } + + return await getRequiredTransactions({ + chain: walletChain, + client: thirdwebClient, + deployMetadata: metadata, + modules: modules?.map((m) => ({ + deployMetadata: m, + })), + }); + }, + enabled: walletChain !== undefined && metadata !== undefined, + }); return ( - - {fullPublishMetadata.isLoading && ( -
    - -
    - )} + <> + + { + if (!walletChain?.id || !activeAccount) { + return; + } - {fullPublishMetadata.isError && ( -
    - - Failed to fetch Publish metadata -
    - )} - - { - if (!selectedChain || !chain) { - return; - } - const deployData = { - ipfsHash, - constructorParams: formData.deployParams, - contractMetadata: formData, - publishMetadata: compilerMetadata.data, - chainId: selectedChain, - is_proxy: fullPublishMetadata.data?.isDeployableViaProxy, - is_factory: fullPublishMetadata.data?.isDeployableViaProxy, - }; - // always respect this since even factory deployments cannot auto-add to registry anymore - const addToDashboard = formData.addToDashboard; - trackEvent({ - category: "custom-contract", - action: "deploy", - label: "attempt", - deployData, - }); - - const deployParams = { ...formData.deployParams }; - - // if Modular contract has modules - if (isModular && modularContractDefaultModulesInstallParams.data) { - const moduleInstallData: string[] = - modularContractDefaultModulesInstallParams.data.map( - (ext, extIndex) => { - return encodeAbiParameters( - // param name+type [] - ext.params.map((p) => ({ name: p.name, type: p.type })), - // value [] - Object.values( - formData.modularContractDefaultModulesInstallParams[ - extIndex - ] || {}, - ), - ); + // open the status modal + let steps: DeployModalStep[] = [ + { + type: "deploy", + signatureCount: deployTransactions.data?.length || 1, + }, + ]; + // if the add to dashboard is checked add that step + if (formData.addToDashboard) { + steps = [ + ...steps, + { + type: "import", + signatureCount: 1, }, - ); - - const moduleInstallationAddresses: string[] = await Promise.all( - modularContractDefaultModulesInstallParams.data.map((d) => - computePublishedContractAddress({ - chain, - client: thirdwebClient, - contractId: d.moduleName, - }), - ), - ); - - deployParams._moduleInstallData = JSON.stringify(moduleInstallData); - deployParams._modules = JSON.stringify(moduleInstallationAddresses); - } - - deploy.mutate( - { - ...formData, - address: walletAddress, - addToDashboard, - deployParams, - }, - { - onSuccess: (deployedContractAddress) => { - console.info("contract deployed:", { - chainId: selectedChain, - address: deployedContractAddress, - }); - - try { - // we don't await this, just kick it off and be done with it - verifyContract({ - contractAddress: deployedContractAddress, - chainId: selectedChain, - }); - } catch { - // ignore - } - - trackEvent({ - category: "custom-contract", - action: "deploy", - label: "success", - deployData, - contractAddress: deployedContractAddress, - addToDashboard, - deployer: connectedWallet, - contractName: compilerMetadata.data?.name, - deployerAndContractName: `${connectedWallet}__${compilerMetadata.data?.name}`, - publisherAndContractName: `${ - fullPublishMetadata.data?.publisher || "deploy-form" - }/${compilerMetadata.data?.name}`, - releaseAsPath: router.asPath, - }); - trackEvent({ - category: "custom-contract", - action: "add-to-dashboard", - label: "success", - contractAddress: deployedContractAddress, - }); - onSuccess(); - if (onSuccessCallback) { - onSuccessCallback(deployedContractAddress || ""); - } else { - invariant( - networkInfo, - `Network not found for chainId ${selectedChain}`, - ); - deployStatusModal.setViewContractLink( - `/${networkInfo.slug}/${deployedContractAddress}`, - ); - } - }, - onError: (err) => { - trackEvent({ - category: "custom-contract", - action: "deploy", - label: "error", - deployData, - error: err, - }); - onError(err); - }, - }, - ); - })} - > - {Object.keys(formDeployParams).length > 0 && ( - <> - {/* Contract Metadata */} - {hasContractURI && ( - - )} - - {/* Primary Sale */} - {hasPrimarySale && ( - - )} - - {hasPrimarySaleRecipient && ( - - )} - - {/* Royalty */} - {hasRoyalty && ( - { - form.setValue("deployParams._royaltyBps", v, { - shouldTouch: true, - }); + ]; + } + deployStatusModal.setViewContractLink(""); + deployStatusModal.open(steps); + try { + // do the actual deployment + const contractAddr = await deployMutation.mutateAsync(formData); + deployStatusModal.nextStep(); + // if add to dashboard is checked, add the contract to the dashboard + if (formData.addToDashboard) { + // add the contract to the dashboard + await addContractToMultiChainRegistry( + { + address: contractAddr, + chainId: walletChain.id, }, - errorMessage: form.getFieldState( - "deployParams._royaltyBps", - form.formState, - ).error?.message, - }} - /> - )} - - {hasPlatformFee && } - - {isSplit && } - - {hasTrustedForwarders && } - - {/* for StakeERC721 */} - {showRewardsSection && ( -
    -
    - {rewardDeployParams.map((paramKey) => { - const deployParam = deployParams.find( - (p) => p.name === paramKey, - ); - - const contructorParams = - fullPublishMetadata.data?.constructorParams || {}; - const extraMetadataParam = contructorParams[paramKey]; - - return ( - - ); - })} -
    -
    - )} - - {/* for Vote */} - {showVoteSection && ( -
    -
    - {voteDeployParams.map((paramKey) => { - const deployParam = deployParams.find( - (p) => p.name === paramKey, - ); - - const contructorParams = - fullPublishMetadata.data?.constructorParams || {}; - const extraMetadataParam = contructorParams[paramKey]; - - return ( - - ); - })} -
    -
    - )} - - {Object.keys(formDeployParams) - .filter((paramName) => { - if ( - isModular && - (paramName === "_moduleInstallData" || - paramName === "_modules") - ) { - return false; - } - - return true; - }) - .map((paramKey) => { - const deployParam = deployParams.find( - (p) => p.name === paramKey, + activeAccount, + 300000n, ); - const contructorParams = - fullPublishMetadata.data?.constructorParams || {}; - const extraMetadataParam = contructorParams[paramKey]; + deployStatusModal.nextStep(); + } + deployStatusModal.setViewContractLink( + `/${walletChain.id}/${contractAddr}`, + ); + } catch (e) { + onError(e); + console.error("failed to deploy contract", e); + deployStatusModal.close(); + } + })} + > + {Object.keys(formDeployParams).length > 0 && ( + <> + {/* Contract Metadata */} + {hasContractURI && } - if (shouldHide(paramKey) || extraMetadataParam?.hidden) { - return null; - } + {/* Primary Sale */} + {hasPrimarySale && ( + + )} - return ( - - ); - })} + {hasPrimarySaleRecipient && ( + + )} - {isModular && ( - <> - {modularContractDefaultModulesInstallParams.data ? ( - - ) : ( -
    - + {/* Royalty */} + {hasRoyalty && ( + { + form.setValue("deployParams._royaltyBps", v, { + shouldTouch: true, + }); + }, + errorMessage: form.getFieldState( + "deployParams._royaltyBps", + form.formState, + ).error?.message, + }} + /> + )} + + {hasPlatformFee && } + + {isSplit && } + + {hasTrustedForwarders && ( + + )} + + {/* for StakeERC721 */} + {showRewardsSection && ( +
    +
    + {rewardDeployParams.map((paramKey) => { + const deployParam = deployParams.find( + (p) => p.name === paramKey, + ); + + const contructorParams = + metadata?.constructorParams || {}; + const extraMetadataParam = contructorParams[paramKey]; + + return ( + + ); + })}
    - )} - - )} +
    + )} - {advancedParams.length > 0 && ( - - - - - Advanced Configuration - + {/* for Vote */} + {showVoteSection && ( +
    +
    + {voteDeployParams.map((paramKey) => { + const deployParam = deployParams.find( + (p) => p.name === paramKey, + ); - - + const contructorParams = + metadata?.constructorParams || {}; + const extraMetadataParam = contructorParams[paramKey]; - - {advancedParams} - - - - )} - - )} + return ( + + ); + })} +
    +
    + )} -
    -
    - {/* Chain */} - - Chain + {Object.keys(formDeployParams) + .filter((paramName) => { + if ( + isModular && + (paramName === "_moduleInstallData" || + paramName === "_modules") + ) { + return false; + } -

    - Select a network to deploy this contract on. We recommend - starting with a testnet.{" "} - - View all chains - -

    + return true; + }) + .map((paramKey) => { + const deployParam = deployParams.find( + (p) => p.name === paramKey, + ); + const contructorParams = metadata?.constructorParams || {}; + const extraMetadataParam = contructorParams[paramKey]; -
    -
    - { - onChainSelect(chain.chainId); - }} - networksEnabled={ - fullPublishMetadata.data?.name === "AccountFactory" || - fullPublishMetadata.data?.networksForDeployment - ?.allNetworks || - !fullPublishMetadata.data?.networksForDeployment - ? undefined - : fullPublishMetadata.data?.networksForDeployment - ?.networksEnabled - } - /> + if (shouldHide(paramKey) || extraMetadataParam?.hidden) { + return null; + } - -
    - - -
    -
    - - {fullPublishMetadata.data?.deployType === "standard" && ( - <> - {/* Deterministic deploy */} - - - form.setValue("deployDeterministic", !!c) - } - /> - -
    - - Deploy at a deterministic address - - -
    -
    -
    - - {/* Optional Salt Input */} - {isCreate2Deployment && ( - - - - - Optional Salt Input - (saltForCreate2) - - - string - - -
    - - - form.setValue("signerAsSalt", !!c) - } + ); + })} + + {isModular && ( + <> + {modules?.length ? ( + + ) : ( +
    + +
    + )} + + )} + + {advancedParams.length > 0 && ( + + + + + Advanced Configuration + + + + + + + {advancedParams} + + + + )} + + )} + +
    +
    + {/* Chain */} + + Chain + +

    + Select a network to deploy this contract on. We recommend + starting with a testnet.{" "} + + View all chains + +

    + +
    +
    + + + +
    +
    +
    + + {metadata?.deployType === "standard" && ( + <> + {/* Deterministic deploy */} + + + form.setValue("deployDeterministic", !!c) + } + /> + +
    + + Deploy at a deterministic address + + +
    +
    +
    + + {/* Optional Salt Input */} + {isCreate2Deployment && ( + + + + + Optional Salt Input + (saltForCreate2) + + + string + + - - Include deployer wallet address in salt (recommended) - - - - )} - - )} +
    + + + form.setValue("signerAsSalt", !!c) + } + /> + + Include deployer wallet address in salt (recommended) + + + + )} + + )} - {/* Import Enable/Disable */} - - - form.setValue("addToDashboard", !!checked) - } - /> - - Import so I can find it in the list of my contracts at{" "} - + + form.setValue("addToDashboard", !!checked) + } + /> + + Import so I can find it in the list of my contracts at{" "} + + /dashboard + + + + + {/* Depoy */} +
    + +
    -
    -
    - - +
    + + - + ); }; - -export default CustomContractForm; diff --git a/apps/dashboard/src/components/contract-components/contract-deploy-form/deploy-form-utils.ts b/apps/dashboard/src/components/contract-components/contract-deploy-form/deploy-form-utils.ts deleted file mode 100644 index 43f5ebca6..000000000 --- a/apps/dashboard/src/components/contract-components/contract-deploy-form/deploy-form-utils.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { thirdwebClient } from "@/constants/client"; -import { upload } from "thirdweb/storage"; - -// biome-ignore lint/suspicious/noExplicitAny: FIXME -export async function uploadContractMetadata(metadata: any) { - const uri = await upload({ client: thirdwebClient, files: [metadata] }); - return uri; -} diff --git a/apps/dashboard/src/components/contract-components/contract-deploy-form/index.tsx b/apps/dashboard/src/components/contract-components/contract-deploy-form/index.tsx deleted file mode 100644 index ac3e51bd3..000000000 --- a/apps/dashboard/src/components/contract-components/contract-deploy-form/index.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { CustomSDKContext } from "contexts/custom-sdk-context"; -import { useSupportedChainsRecord } from "hooks/chains/configureChains"; -import { useEffect, useState } from "react"; -import { useActiveAccount, useActiveWalletChain } from "thirdweb/react"; -import type { ContractId } from "../types"; -import CustomContractForm from "./custom-contract"; - -interface ContractDeployFormProps { - contractId: ContractId; - version?: string; - chainId?: number; - onSuccessCallback?: (contractAddress: string) => void; - isImplementationDeploy?: true; -} - -export const ContractDeployForm: React.FC = ({ - contractId, - version, - chainId: chainIdProp, - onSuccessCallback, - isImplementationDeploy, -}) => { - const walletAddress = useActiveAccount()?.address; - const connectedChainId = useActiveWalletChain()?.id; - const configuredNetworksRecord = useSupportedChainsRecord(); - const [selectedChain, setSelectedChain] = useState( - chainIdProp - ? chainIdProp - : connectedChainId && connectedChainId in configuredNetworksRecord - ? connectedChainId - : undefined, - ); - - // FIXME: all of this logic needs to be reworked - // eslint-disable-next-line no-restricted-syntax - useEffect(() => { - // If the user has not selected a chain, and the connected chain is configured, select it - if ( - !selectedChain && - connectedChainId && - connectedChainId in configuredNetworksRecord - ) { - setSelectedChain(connectedChainId); - } - }, [connectedChainId, selectedChain, configuredNetworksRecord]); - - if (!contractId) { - return null; - } - if (contractId === "custom") { - return
    Invalid attempt to deploy "custom" contract.
    ; - } - - return ( - - not be overwritten -> not trigger a re-render - // we can clean this up once the we can can call functions without constructing a whole new SDK each time - walletAddress={walletAddress} - /> - - ); -}; diff --git a/apps/dashboard/src/components/contract-components/contract-deploy-form/modular-contract-default-modules-fieldset.tsx b/apps/dashboard/src/components/contract-components/contract-deploy-form/modular-contract-default-modules-fieldset.tsx index ed3b4bb91..05d85599d 100644 --- a/apps/dashboard/src/components/contract-components/contract-deploy-form/modular-contract-default-modules-fieldset.tsx +++ b/apps/dashboard/src/components/contract-components/contract-deploy-form/modular-contract-default-modules-fieldset.tsx @@ -1,72 +1,33 @@ import { FormControl } from "@chakra-ui/react"; -import { useQuery } from "@tanstack/react-query"; import { SolidityInput } from "contract-ui/components/solidity-inputs"; -import { getModuleInstalledParams } from "contract-ui/tabs/manage/components/getModuleInstalledParams"; -import invariant from "tiny-invariant"; +import { useMemo } from "react"; +import type { FetchDeployMetadataResult } from "thirdweb/contract"; import { FormErrorMessage, FormLabel } from "tw-components"; import type { CustomContractDeploymentForm } from "./custom-contract"; import { PrimarySaleFieldset } from "./primary-sale-fieldset"; import { RoyaltyFieldset } from "./royalty-fieldset"; -type ModuleMeta = { - moduleName: string; - moduleVersion: string; - publisherAddress: string; -}; - -/** - * Get the install params for all given modules - */ -export function useModularContractsDefaultModulesInstallParams(props: { - defaultModules?: ModuleMeta[]; - isQueryEnabled: boolean; -}) { - const { defaultModules, isQueryEnabled } = props; - return useQuery({ - queryKey: [ - "useModularContractsDefaultModulesInstallParams", - defaultModules, - ], - queryFn: async () => { - invariant(defaultModules, "defaultModules must be defined"); - return Promise.all(defaultModules.map(getModuleInstalledParams)); - }, - enabled: !!(isQueryEnabled && defaultModules), - refetchOnWindowFocus: false, - }); +export function getModuleInstallParams(mod: FetchDeployMetadataResult) { + return ( + mod.abi + .filter((a) => a.type === "function") + .find((f) => f.name === "encodeBytesOnInstall")?.inputs || [] + ); } -export type UseModularContractsDefaultModulesInstallParams = ReturnType< - typeof useModularContractsDefaultModulesInstallParams ->; - -type Modules = NonNullable< - UseModularContractsDefaultModulesInstallParams["data"] ->; - -type ModuleWithIndex = Modules[number] & { moduleIndex: number }; - export function ModularContractDefaultModulesFieldset(props: { - modules: Modules; + modules: FetchDeployMetadataResult[]; form: CustomContractDeploymentForm; isTWPublisher: boolean; }) { - // save the index of the module before filtering out - const modulesWithIndex: ModuleWithIndex[] = props.modules - .map((v, i) => ({ - ...v, - moduleIndex: i, - })) - .filter((v) => v.params.length > 0); - return (
    - {modulesWithIndex.map((ext) => { + {props.modules.map((mod) => { return ( @@ -78,15 +39,22 @@ export function ModularContractDefaultModulesFieldset(props: { } function RenderModule(props: { - module: ModuleWithIndex; + module: FetchDeployMetadataResult; form: CustomContractDeploymentForm; isTWPublisher: boolean; }) { const { module, form } = props; + const params = useMemo(() => getModuleInstallParams(module), [module]); + + if (params.length === 0) { + // if a module has no params, don't render anything + return null; + } + // only consider mapping if published by thirdweb, else show the generic form if (props.isTWPublisher) { - const paramNames = module.params.map((param) => param.name); + const paramNames = params.map((p) => p.name).filter((n) => n !== undefined); if (showRoyaltyFieldset(paramNames)) { return ( @@ -104,12 +72,12 @@ function RenderModule(props: { return (

    - {module.moduleName} + {module.name}

    - {module.params.map((param) => { + {params.map((param) => { const formFieldKey = - `modularContractDefaultModulesInstallParams.${module.moduleIndex}.${param.name}` as const; + `moduleData.${module.name}.${param.name}` as const; return ( {param.name} = ({ form }) => { - const defaultForwarders = useDefaultForwarders(); - return (
    - - - - { form.getFieldState( diff --git a/apps/dashboard/src/components/contract-components/fetch-contracts-with-versions.ts b/apps/dashboard/src/components/contract-components/fetch-contracts-with-versions.ts new file mode 100644 index 000000000..099eb0434 --- /dev/null +++ b/apps/dashboard/src/components/contract-components/fetch-contracts-with-versions.ts @@ -0,0 +1,110 @@ +import { thirdwebClient } from "@/constants/client"; +import type { ProfileMetadata } from "constants/schemas"; +import { type ThirdwebContract, getContract, isAddress } from "thirdweb"; +import { polygon } from "thirdweb/chains"; +import { fetchDeployMetadata } from "thirdweb/contract"; +import { resolveAddress } from "thirdweb/extensions/ens"; +import { + getPublishedContractVersions, + getPublisherProfileUri, +} from "thirdweb/extensions/thirdweb"; +import { download } from "thirdweb/storage"; + +function mapThirdwebPublisher(publisher: string) { + if (publisher === "thirdweb.eth") { + return "deployer.thirdweb.eth"; + } + return publisher; +} + +let publisherContract: ThirdwebContract; +function getPublisherContract() { + if (!publisherContract) { + publisherContract = getContract({ + client: thirdwebClient, + address: "0xf5b896Ddb5146D5dA77efF4efBb3Eae36E300808", + chain: polygon, + }); + } + return publisherContract; +} + +export async function fetchPublisherProfile(publisherAddress: string) { + const profileUri = await getPublisherProfileUri({ + contract: getPublisherContract(), + publisher: isAddress(publisherAddress) + ? publisherAddress + : await resolveAddress({ + client: thirdwebClient, + name: mapThirdwebPublisher(publisherAddress), + }), + }); + if (!profileUri) { + return null; + } + try { + const res = await download({ + client: thirdwebClient, + uri: profileUri, + }); + return res.json() as Promise; + } catch { + return null; + } +} + +export async function fetchPublishedContractVersions( + publisherAddress: string, + contractId: string, +) { + const allVersions = await getPublishedContractVersions({ + contract: getPublisherContract(), + publisher: isAddress(publisherAddress) + ? publisherAddress + : await resolveAddress({ + client: thirdwebClient, + name: mapThirdwebPublisher(publisherAddress), + }), + contractId: contractId, + }); + + const sortedVersions = allVersions.toSorted((a, b) => { + if (a.publishTimestamp === b.publishTimestamp) { + return 0; + } + return a.publishTimestamp > b.publishTimestamp ? -1 : 1; + }); + + const responses = await Promise.allSettled( + sortedVersions.map((v) => + fetchDeployMetadata({ + client: thirdwebClient, + uri: v.publishMetadataUri, + }).then((m) => ({ ...m, ...v })), + ), + ); + + return responses.filter((r) => r.status === "fulfilled").map((r) => r.value); +} + +export async function fetchPublishedContractVersion( + publisherAddress: string, + contractId: string, + version = "latest", +) { + const allVersions = await fetchPublishedContractVersions( + publisherAddress, + contractId, + ); + if (allVersions.length === 0) { + return null; + } + if (version === "latest") { + return allVersions[0]; + } + return allVersions.find((v) => v.version === version) || allVersions[0]; +} + +export type PublishedContractWithVersion = Awaited< + ReturnType +>[number]; diff --git a/apps/dashboard/src/components/contract-components/hooks.ts b/apps/dashboard/src/components/contract-components/hooks.ts index a909088eb..6c66198c7 100644 --- a/apps/dashboard/src/components/contract-components/hooks.ts +++ b/apps/dashboard/src/components/contract-components/hooks.ts @@ -1,127 +1,43 @@ import { thirdwebClient } from "@/constants/client"; -import { - contractKeys, - networkKeys, - useDashboardEVMChainId, -} from "@3rdweb-sdk/react"; +import { networkKeys, useDashboardEVMChainId } from "@3rdweb-sdk/react"; import { useMutationWithInvalidate } from "@3rdweb-sdk/react/hooks/query/useQueryWithNetwork"; import { type QueryClient, - useMutation, useQuery, useQueryClient, } from "@tanstack/react-query"; -import { useSDK, useSigner } from "@thirdweb-dev/react"; +import { useSDK } from "@thirdweb-dev/react"; import { - type DeploymentTransaction, type ExtraPublishMetadata, type FeatureName, type FeatureWithEnabled, type PublishedContract, type ThirdwebSDK, detectFeatures, - fetchAndCacheDeployMetadata, fetchContractMetadata, fetchPreDeployMetadata, - fetchRawPredeployMetadata, - getTrustedForwarders, isExtensionEnabled, } from "@thirdweb-dev/sdk"; -import { - getZkTransactionsForDeploy, - zkDeployContractFromUri, -} from "@thirdweb-dev/sdk/evm/zksync"; import type { Abi } from "abitype"; import type { ProfileMetadata, ProfileMetadataInput } from "constants/schemas"; -import type { providers } from "ethers"; import { useSupportedChain } from "hooks/chains/configureChains"; import { isEnsName, resolveEns } from "lib/ens"; import { getDashboardChainRpc } from "lib/rpc"; import { StorageSingleton, getThirdwebSDK } from "lib/sdk"; -import { useRouter } from "next/router"; import { useMemo } from "react"; import { getContract } from "thirdweb"; -import { - abstractTestnet, - polygon, - zkCandySepolia, - zkSync, - zkSyncSepolia, -} from "thirdweb/chains"; +import { polygon } from "thirdweb/chains"; import { resolveContractAbi } from "thirdweb/contract"; -import { - useActiveAccount, - useActiveWallet, - useActiveWalletChain, -} from "thirdweb/react"; +import { useActiveAccount } from "thirdweb/react"; import { isAddress } from "thirdweb/utils"; import invariant from "tiny-invariant"; -import { Web3Provider } from "zksync-ethers"; import { useV5DashboardChain } from "../../lib/v5-adapter"; -import type { CustomContractDeploymentFormData } from "./contract-deploy-form/custom-contract"; -import type { - DeployModalStep, - DeployStatusModal, -} from "./contract-deploy-form/deploy-context-modal"; -import { uploadContractMetadata } from "./contract-deploy-form/deploy-form-utils"; +import type {} from "./contract-deploy-form/deploy-context-modal"; +import { + type PublishedContractWithVersion, + fetchPublishedContractVersions, +} from "./fetch-contracts-with-versions"; import type { ContractId } from "./types"; -import { addContractToMultiChainRegistry } from "./utils"; - -function isChainIdZkSync(chainId?: number) { - switch (chainId) { - case zkSync.id: - case zkSyncSepolia.id: - case zkCandySepolia.id: - case abstractTestnet.id: - // TODO - add more ZK chains here - return true; - default: - return false; - } -} - -// metadata PRE publish, only has the compiler output info (from CLI) -export async function fetchRawPredeployMetadataFromURI(contractId: ContractId) { - const contractIdIpfsHash = toContractIdIpfsHash(contractId); - - invariant(contractId !== "ipfs://undefined", "uri can't be undefined"); - let resolved: - | Awaited> - | undefined = undefined; - try { - resolved = await fetchRawPredeployMetadata( - contractIdIpfsHash, - StorageSingleton, - ); - } catch (err) { - console.error("failed to resolvePreDeployMetadata", err); - } - - if (!resolved) { - return { - name: "", - metadataUri: "", - bytecodeUri: "", - }; - } - - return { - name: resolved.name, - metadataUri: resolved.metadataUri, - bytecodeUri: resolved.bytecodeUri, - analytics: removeUndefinedFromObject(resolved.analytics), - compilers: resolved.compilers, - }; -} - -export function useContractRawPredeployMetadataFromURI(contractId: ContractId) { - return useQuery({ - queryKey: ["raw-predeploy-metadata", contractId], - queryFn: () => fetchRawPredeployMetadataFromURI(contractId), - - enabled: !!contractId, - }); -} // biome-ignore lint/suspicious/noExplicitAny: FIXME function removeUndefinedFromObject(obj: Record) { @@ -136,9 +52,7 @@ function removeUndefinedFromObject(obj: Record) { } // metadata PRE publish, only has the compiler output info (from CLI) -export async function fetchContractPublishMetadataFromURI( - contractId: ContractId, -) { +async function fetchContractPublishMetadataFromURI(contractId: ContractId) { const contractIdIpfsHash = toContractIdIpfsHash(contractId); invariant(contractId !== "ipfs://undefined", "uri can't be undefined"); @@ -181,21 +95,6 @@ export function useContractPublishMetadataFromURI(contractId: ContractId) { }); } -export function useDefaultForwarders() { - const sdk = useSDK(); - const provider = sdk?.getProvider(); - invariant(provider, "Require provider"); - - const chainId = useDashboardEVMChainId(); - - return useQuery({ - queryKey: ["default-forwarders", chainId], - queryFn: () => { - return getTrustedForwarders(provider, StorageSingleton); - }, - }); -} - // 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) { @@ -295,63 +194,21 @@ export function usePublisherProfile(publisherAddress?: string) { return useQuery(publisherProfileQuery(publisherAddress)); } -export async function fetchAllVersions( - sdk?: ThirdwebSDK, - publisherAddress?: string, - contractName?: string, -) { - invariant(publisherAddress, "address is not defined"); - invariant(contractName, "contract name is not defined"); - invariant(sdk, "sdk not provided"); - - const allVersions = await sdk - .getPublisher() - .getAllVersions(publisherAddress, contractName); - - const publishedVersions = []; - - for (let i = 0; i < allVersions.length; i++) { - const contractInfo = await sdk - .getPublisher() - .fetchPublishedContractInfo(allVersions[i]) - .catch(() => { - console.error( - `failed to fetchPublishedContractInfo for metadataUri: ${allVersions[i].metadataUri} - ignoring version`, - ); - return null; - }); - if (!contractInfo) { - continue; - } - - publishedVersions.unshift({ - ...allVersions[i], - version: contractInfo.publishedMetadata.version, - name: contractInfo.publishedMetadata.name, - displayName: contractInfo.publishedMetadata.displayName || "", - description: contractInfo.publishedMetadata.description || "", - publisher: contractInfo.publishedMetadata.publisher || "", - audit: contractInfo.publishedMetadata.audit || "", - logo: contractInfo.publishedMetadata.logo || "", - }); - } - - return publishedVersions; -} - export function useAllVersions( - publisherAddress?: string, - contractName?: string, + publisherAddress: string | undefined, + contractId: string | undefined, ) { - const sdk = getThirdwebSDK( - polygon.id, - getDashboardChainRpc(polygon.id, undefined), - ); return useQuery({ - queryKey: ["all-releases", publisherAddress, contractName], - queryFn: () => fetchAllVersions(sdk, publisherAddress, contractName), + queryKey: ["all-releases", publisherAddress, contractId], + queryFn: () => { + if (!publisherAddress || !contractId) { + // should never happen because we check for this in the enabled check + throw new Error("publisherAddress or contractId is not defined"); + } + return fetchPublishedContractVersions(publisherAddress, contractId); + }, - enabled: !!publisherAddress && !!contractName && !!sdk, + enabled: !!publisherAddress && !!contractId, }); } @@ -398,82 +255,86 @@ export function usePublishedContractsFromDeploy( }); } +function publishedContractWithVersionToLegacy( + input: PublishedContractWithVersion, +) { + return { + ...input, + timestamp: input.publishTimestamp.toString(), + id: input.contractId, + metadataUri: input.publishMetadataUri, + } satisfies PublishedContract; +} + export async function fetchPublishedContractInfo( sdk?: ThirdwebSDK, - contract?: PublishedContract, + contract?: PublishedContractWithVersion, ) { invariant(contract, "contract is not defined"); invariant(sdk, "sdk not provided"); - return await sdk.getPublisher().fetchPublishedContractInfo(contract); + return await sdk + .getPublisher() + .fetchPublishedContractInfo(publishedContractWithVersionToLegacy(contract)); } -export function usePublishedContractInfo(contract: PublishedContract) { +export function usePublishedContractInfo( + contract: PublishedContractWithVersion, +) { const sdk = getThirdwebSDK( polygon.id, getDashboardChainRpc(polygon.id, undefined), ); return useQuery({ queryKey: ["released-contract", contract], - queryFn: () => fetchPublishedContractInfo(sdk, contract), - + queryFn: () => + fetchPublishedContractInfo( + sdk, + publishedContractWithVersionToLegacy(contract), + ), enabled: !!contract, }); } -export function usePublishedContractFunctions(contract: PublishedContract) { - const publishedContractInfo = usePublishedContractInfo(contract); - const compositeAbi = - publishedContractInfo.data?.publishedMetadata.compositeAbi; +export function usePublishedContractFunctions( + publishedContract: PublishedContractWithVersion, +) { + const compositeAbi = publishedContract.compositeAbi; - const { data: meta } = useContractPublishMetadataFromURI( - contract.metadataUri, - ); - - const dynamicContractType = - publishedContractInfo.data?.publishedMetadata.routerType; + const dynamicContractType = publishedContract.routerType; if ( compositeAbi && (dynamicContractType === "plugin" || dynamicContractType === "dynamic" || - !publishedContractInfo.data?.publishedMetadata.deployType || - publishedContractInfo.data?.publishedMetadata.name.includes( - "MarketplaceV3", - )) + !publishedContract.deployType || + publishedContract.name.includes("MarketplaceV3")) ) { return compositeAbi.filter((f) => f.type === "function"); } - return meta?.abi?.filter((f) => f.type === "function") || []; + return publishedContract?.abi?.filter((f) => f.type === "function") || []; } -export function usePublishedContractEvents(contract: PublishedContract) { - const publishedContractInfo = usePublishedContractInfo(contract); - const compositeAbi = - publishedContractInfo.data?.publishedMetadata.compositeAbi; +export function usePublishedContractEvents( + publishedContract: PublishedContractWithVersion, +) { + const compositeAbi = publishedContract.compositeAbi; - const { data: meta } = useContractPublishMetadataFromURI( - contract.metadataUri, - ); - - const dynamicContractType = - publishedContractInfo.data?.publishedMetadata.routerType; + const dynamicContractType = publishedContract.routerType; if ( compositeAbi && (dynamicContractType === "plugin" || dynamicContractType === "dynamic" || - !publishedContractInfo.data?.publishedMetadata.deployType || - publishedContractInfo.data?.publishedMetadata.name.includes( - "MarketplaceV3", - )) + !publishedContract.deployType || + publishedContract.name.includes("MarketplaceV3")) ) { return compositeAbi.filter((f) => f.type === "event"); } - return meta?.abi?.filter((f) => f.type === "event") || []; + return publishedContract?.abi?.filter((f) => f.type === "event") || []; } export function usePublishedContractCompilerMetadata( - contract: PublishedContract, + contract: PublishedContractWithVersion, ) { - return useContractPublishMetadataFromURI(contract.metadataUri); + return useContractPublishMetadataFromURI(contract.publishMetadataUri); } export function useFunctionParamsFromABI(abi?: Abi, functionName?: string) { @@ -549,393 +410,6 @@ export function useEditProfileMutation() { ); } -type ContractDeployMutationParams = CustomContractDeploymentFormData & { - address: string | undefined; - addToDashboard: boolean; -}; - -export function useCustomContractDeployMutation(options: { - ipfsHash: string; - version?: string; - forceDirectDeploy?: boolean; - hasContractURI?: boolean; - hasRoyalty?: boolean; - isSplit?: boolean; - isVote?: boolean; - isErc721SharedMetadadata?: boolean; - deployStatusModal: DeployStatusModal; -}) { - const { - ipfsHash, - version, - hasContractURI, - hasRoyalty, - isSplit, - isVote, - isErc721SharedMetadadata, - deployStatusModal, - forceDirectDeploy, - } = options; - const sdk = useSDK(); - const queryClient = useQueryClient(); - const account = useActiveAccount(); - const walletAddress = account?.address; - const chainId = useActiveWalletChain()?.id; - const signer = useSigner(); - const { data: transactions } = useTransactionsForDeploy(ipfsHash); - const fullPublishMetadata = useContractFullPublishMetadata(ipfsHash); - const rawPredeployMetadata = useContractRawPredeployMetadataFromURI(ipfsHash); - const router = useRouter(); - - const walletId = useActiveWallet()?.id; - - return useMutation( - async (_data: ContractDeployMutationParams) => { - const data = { ..._data }; - - invariant( - sdk && "getPublisher" in sdk, - "sdk is not ready or does not support publishing", - ); - invariant(signer, "signer is not provided"); - invariant(walletAddress, "walletAddress is not provided"); - - const requiresSignature = walletId !== "inApp"; - - const stepDeploy: DeployModalStep = { - signatureCount: requiresSignature ? transactions?.length || 1 : 0, - type: "deploy", - }; - - const steps = [stepDeploy]; - - if (isErc721SharedMetadadata) { - steps.push({ - type: "setNFTMetadata", - signatureCount: requiresSignature ? 1 : 0, - }); - } - - if (data.addToDashboard) { - steps.push({ - type: "import", - signatureCount: requiresSignature ? 1 : 0, - }); - } - - // open the modal with the appropriate steps - deployStatusModal.open(steps); - - const isZkSync = isChainIdZkSync(chainId); - - let contractAddress: string; - try { - if (hasContractURI) { - data.deployParams._contractURI = await uploadContractMetadata({ - ...data.contractMetadata, - ...(hasRoyalty && { - seller_fee_basis_points: - typeof data.deployParams._royaltyBps === "string" - ? Number.parseInt(data.deployParams._royaltyBps, 10) - : data.deployParams?._royaltyBps || 0, - }), - ...(hasRoyalty && { - fee_recipient: data.deployParams._royaltyRecipient, - }), - ...(isSplit && { - recipients: data.recipients, - }), - ...(isVote && { - voting_delay_in_blocks: Number( - data.deployParams._initialVotingDelay, - ), - voting_period_in_blocks: Number( - data.deployParams._initialVotingPeriod, - ), - voting_token_address: data.deployParams._token, - voting_quorum_fraction: Number( - data.deployParams._initialVoteQuorumFraction, - ), - proposal_token_threshold: - data.deployParams._initialProposalThreshold, - }), - }); - if ("_name" in data.deployParams) { - data.deployParams._name = data.contractMetadata?.name || ""; - } - if ("_symbol" in data.deployParams) { - data.deployParams._symbol = data.contractMetadata?.symbol || ""; - } - } - - if (isSplit) { - data.deployParams._payees = JSON.stringify( - data.recipients?.map((r) => r.address) || [], - ); - data.deployParams._shares = JSON.stringify( - data.recipients?.map((r) => r.sharesBps) || [], - ); - } - - if (data.deployParams?._defaultAdmin === "") { - data.deployParams._defaultAdmin = data.address || ""; - } - - // Handle ZkSync deployments separately - - // deploy contract - if (isZkSync) { - // Get metamask signer using zksync-ethers library -- for custom fields in signature - let deploySigner = signer; - - if (fullPublishMetadata?.data?.deployType === "standard") { - // if its a direct deploy AND its zksync, use the ethers zksync signer for now - // NOTE: this only works with injected wallets - // TODO - implement deploying to zksync in v5 account and remove this - const fakeExternalProvider: providers.ExternalProvider = { - // fake tell it its metamask always (lul) - isMetaMask: true, - request({ method, params }) { - switch (method) { - case "eth_accounts": { - return Promise.resolve([walletAddress]); - } - case "eth_signTypedData_v4": { - invariant(params?.[1], "invalid signTypedData call"); - // yo dawg, I heard you like signing typed data - const { domain, types, message, primaryType } = JSON.parse( - params[1], - ); - return (signer as providers.JsonRpcSigner)._signTypedData( - domain, - // don't ask... - { [primaryType]: types[primaryType] }, - message, - ); - } - case "eth_sendTransaction": { - const tx = params?.[0]; - if (!tx) { - throw new Error("No transaction provided"); - } - return signer.sendTransaction(tx); - } - case "personal_sign": { - const data = params?.[0]; - if (!data) { - throw new Error("Nothing to sign"); - } - return signer.signMessage(data); - } - default: { - return (signer as providers.JsonRpcSigner)?.provider?.send( - method, - params ?? [], - ); - } - } - }, - }; - const zkSigner = new Web3Provider(fakeExternalProvider).getSigner(); - deploySigner = zkSigner; - } - - const publishUri = ipfsHash.startsWith("ipfs://") - ? ipfsHash - : `ipfs://${ipfsHash}`; - - let uriToRegister = ""; - - if ( - fullPublishMetadata?.data?.compilers?.zksolc || - rawPredeployMetadata?.data?.compilers?.zksolc - ) { - if (data.deployDeterministic) { - const salt = data.signerAsSalt - ? (await signer?.getAddress())?.concat( - data.saltForCreate2 || "", - ) - : data.saltForCreate2; - - contractAddress = await zkDeployContractFromUri( - publishUri, - Object.values(data.deployParams), - deploySigner, - StorageSingleton, - chainId as number, - { - compilerOptions: { - compilerType: "zksolc", - }, - saltForProxyDeploy: salt, - }, - true, - ); - } else { - contractAddress = await zkDeployContractFromUri( - publishUri, - Object.values(data.deployParams), - deploySigner, - StorageSingleton, - chainId as number, - { - compilerOptions: { - compilerType: "zksolc", - }, - }, - ); - } - - const { compilerMetadata } = await fetchAndCacheDeployMetadata( - publishUri, - StorageSingleton, - { - compilerType: "zksolc", - }, - ); - uriToRegister = compilerMetadata.fetchedMetadataUri; - } else { - contractAddress = await zkDeployContractFromUri( - publishUri, - Object.values(data.deployParams), - signer, - StorageSingleton, - chainId as number, - ); - - const { compilerMetadata } = await fetchAndCacheDeployMetadata( - publishUri, - StorageSingleton, - ); - uriToRegister = compilerMetadata.fetchedMetadataUri; - } - - // register deployed zksync contract on multichain registry - await addContractToMultiChainRegistry( - { - address: contractAddress, - chainId: chainId as number, - metadataURI: uriToRegister, - }, - account, - 300000n, - ); - } else { - if (data.deployDeterministic) { - const salt = data.signerAsSalt - ? (await signer?.getAddress())?.concat(data.saltForCreate2 || "") - : data.saltForCreate2; - contractAddress = - await sdk.deployer.deployPublishedContractDeterministic( - fullPublishMetadata.data?.name as string, - Object.values(data.deployParams), - fullPublishMetadata.data?.publisher as string, - // this is either the contract version or it falls back to "latest" - version, - salt, - ); - } else { - contractAddress = await sdk.deployer.deployContractFromUri( - ipfsHash.startsWith("ipfs://") ? ipfsHash : `ipfs://${ipfsHash}`, - Object.values(data.deployParams), - { - forceDirectDeploy, - defaultModules: data.defaultModules, - }, - ); - } - } - - deployStatusModal.nextStep(); - } catch (e) { - // failed to deploy contract - close modal for now - deployStatusModal.close(); - // re-throw error - throw e; - } - - const contract = await sdk.getContract(contractAddress); - - if (isErc721SharedMetadadata) { - try { - await contract.erc721.sharedMetadata.set({ - name: data.contractMetadata?.name || "", - description: data.contractMetadata?.description || "", - image: data.contractMetadata?.image || "", - }); - - deployStatusModal.nextStep(); - } catch { - // failed to set metadata - for now just close the modal - deployStatusModal.close(); - // not re-throwing the error, this is not technically a failure to deploy, just to set metadata - the contract is deployed already at this stage - } - } - - try { - // let user decide if they want this or not - if (data.addToDashboard && !isZkSync) { - invariant(chainId, "chainId is not provided"); - await addContractToMultiChainRegistry( - { - address: contractAddress, - chainId, - }, - account, - 300000n, - ); - - deployStatusModal.nextStep(); - } - } catch { - // failed to add to dashboard - for now just close the modal - deployStatusModal.close(); - router.replace(`/${chainId}/${contractAddress}`); - - // not re-throwing the error, this is not technically a failure to deploy, just to add to dashboard - the contract is deployed already at this stage - } - - return contractAddress; - }, - { - onSuccess: async () => { - return await queryClient.invalidateQueries([ - ...networkKeys.chain(chainId), - ...contractKeys.list(walletAddress), - [networkKeys.multiChainRegistry, walletAddress], - ]); - }, - }, - ); -} - -export function useTransactionsForDeploy(publishMetadataOrUri: string) { - const sdk = useSDK(); - const chainId = useActiveWalletChain()?.id; - - const queryResult = useQuery({ - queryKey: ["transactions-for-deploy", publishMetadataOrUri, chainId], - queryFn: async () => { - invariant(sdk, "sdk not provided"); - - // Handle separately for ZkSync - if (isChainIdZkSync(chainId)) { - return await getZkTransactionsForDeploy(); - } - - return await sdk.deployer.getTransactionsForDeploy( - publishMetadataOrUri.startsWith("ipfs://") - ? publishMetadataOrUri - : `ipfs://${publishMetadataOrUri}`, - ); - }, - - enabled: !!publishMetadataOrUri && !!sdk, - }); - - return queryResult; -} - export async function fetchPublishedContracts( sdk: ThirdwebSDK, queryClient: QueryClient, diff --git a/apps/dashboard/src/components/contract-components/published-contract/index.tsx b/apps/dashboard/src/components/contract-components/published-contract/index.tsx index 93cc10685..10264aaa9 100644 --- a/apps/dashboard/src/components/contract-components/published-contract/index.tsx +++ b/apps/dashboard/src/components/contract-components/published-contract/index.tsx @@ -1,3 +1,5 @@ +"use client"; + import { Divider, Flex, @@ -6,199 +8,118 @@ import { List, ListItem, } from "@chakra-ui/react"; -import { SiTwitter } from "@react-icons/all-files/si/SiTwitter"; import { useQuery } from "@tanstack/react-query"; -import { - type PublishedContract as PublishedContractType, - type PublishedMetadata, - fetchSourceFilesFromMetadata, -} from "@thirdweb-dev/sdk"; -import type { Abi } from "abitype"; import { ContractFunctionsOverview } from "components/contract-functions/contract-functions"; -import { replaceDeployerAddress } from "components/explore/publisher"; -import { ShareButton } from "components/share-buttom"; -import { THIRDWEB_DOMAIN } from "constants/urls"; -import { Extensions } from "contract-ui/tabs/overview/components/Extensions"; import { format } from "date-fns/format"; import { correctAndUniqueLicenses } from "lib/licenses"; -import { StorageSingleton, replaceIpfsUrl } from "lib/sdk"; -import { getAbsoluteUrl } from "lib/vercel-utils"; -import { NextSeo } from "next-seo"; -import Head from "next/head"; -import { useRouter } from "next/router"; -import { PublishedContractOG } from "og-lib/url-utils"; +import { replaceIpfsUrl } from "lib/sdk"; import { useMemo } from "react"; import { BiPencil } from "react-icons/bi"; import { BsShieldCheck } from "react-icons/bs"; import { VscBook, VscCalendar, VscServer } from "react-icons/vsc"; import { useActiveAccount } from "thirdweb/react"; +import { download } from "thirdweb/storage"; import invariant from "tiny-invariant"; +import { Card, Heading, Link, LinkButton, Text } from "tw-components"; +import { thirdwebClient } from "../../../@/constants/client"; +import type { PublishedContractWithVersion } from "../fetch-contracts-with-versions"; import { - Card, - Heading, - Link, - LinkButton, - Text, - TrackedIconButton, -} from "tw-components"; -import { shortenIfAddress } from "utils/usedapp-external"; -import { - useContractEnabledExtensions, - useContractPublishMetadataFromURI, - useEns, - usePublishedContractCompilerMetadata, usePublishedContractEvents, usePublishedContractFunctions, - usePublishedContractInfo, - usePublisherProfile, } from "../hooks"; import { PublisherHeader } from "../publisher/publisher-header"; import { AddressesModal } from "./addresses-modal"; import { MarkdownRenderer } from "./markdown-renderer"; -export interface ExtendedPublishedContract extends PublishedContractType { +export interface ExtendedPublishedContract + extends PublishedContractWithVersion { name: string; displayName?: string; - description: string; - version: string | undefined; - publisher: string; + description?: string; + version?: string; + publisher?: string; tags?: string[]; logo?: string; audit?: string; } interface PublishedContractProps { - contract: ExtendedPublishedContract; + publishedContract: ExtendedPublishedContract; walletOrEns: string; } export const PublishedContract: React.FC = ({ - contract, + publishedContract, walletOrEns, }) => { const address = useActiveAccount()?.address; - const publishedContractInfo = usePublishedContractInfo(contract); - const { data: compilerInfo } = usePublishedContractCompilerMetadata(contract); - const router = useRouter(); - const contractPublishMetadata = useContractPublishMetadataFromURI( - contract.metadataUri, - ); - - const dynamicContractType = - publishedContractInfo.data?.publishedMetadata.routerType; - - const compositeAbi = - publishedContractInfo.data?.publishedMetadata.compositeAbi; - - const abi = - compositeAbi && - (dynamicContractType === "plugin" || - dynamicContractType === "dynamic" || - !publishedContractInfo.data?.publishedMetadata.deployType || - publishedContractInfo.data?.publishedMetadata.name.includes( - "MarketplaceV3", - )) - ? compositeAbi - : contractPublishMetadata.data?.abi; - - const enabledExtensions = useContractEnabledExtensions( - abi as Abi | undefined, - ); - - const publisherProfile = usePublisherProfile(contract.publisher); - - const currentRoute = `${THIRDWEB_DOMAIN}${router.asPath.replace( - "/publish", - "", - )}`.replace("deployer.thirdweb.eth", "thirdweb.eth"); - - const contractFunctions = usePublishedContractFunctions(contract); - const contractEvents = usePublishedContractEvents(contract); - - const ensQuery = useEns(contract.publisher); - - const publisherEnsOrAddress = replaceDeployerAddress( - shortenIfAddress(ensQuery.data?.ensName || contract.publisher), - ); + const contractFunctions = usePublishedContractFunctions(publishedContract); + const contractEvents = usePublishedContractEvents(publishedContract); const publishDate = format( new Date( - Number.parseInt( - publishedContractInfo?.data?.publishedTimestamp.toString() || "0", - ) * 1000, + Number.parseInt(publishedContract?.publishTimestamp.toString() || "0") * + 1000, ), "MMM dd, yyyy", ); - const licenses = correctAndUniqueLicenses(compilerInfo?.licenses || []); + const licenses = correctAndUniqueLicenses(publishedContract?.licenses || []); - const publishedContractName = useMemo(() => { - if (contract.displayName) { - return contract.displayName; - } - return contract.name; - }, [contract.displayName, contract.name]); + // const publishedContractName = + // publishedContract?.displayName || publishedContract?.name; - const extensionNames = useMemo(() => { - return enabledExtensions.map((ext) => ext.name); - }, [enabledExtensions]); + // const extensionNames = useMemo(() => { + // return enabledExtensions.map((ext) => ext.name); + // }, [enabledExtensions]); - const ogImageUrl = useMemo( - () => - PublishedContractOG.toUrl({ - name: publishedContractName, - description: contract.description, - version: contract.version || "latest", - publisher: publisherEnsOrAddress, - extension: extensionNames, - license: licenses, - publishDate, - publisherAvatar: publisherProfile.data?.avatar || undefined, - logo: contract.logo, - }), - [ - extensionNames, - licenses, - contract.description, - contract.logo, - publishedContractName, - contract.version, - publishDate, - publisherEnsOrAddress, - publisherProfile.data?.avatar, - ], - ); + // const ogImageUrl = useMemo( + // () => + // PublishedContractOG.toUrl({ + // name: publishedContractName, + // description: contract.description, + // version: contract.version || "latest", + // publisher: publisherEnsOrAddress, + // extension: extensionNames, + // license: licenses, + // publishDate, + // publisherAvatar: publisherProfile.data?.avatar || undefined, + // logo: contract.logo, + // }), + // [ + // extensionNames, + // licenses, + // contract.description, + // contract.logo, + // publishedContractName, + // contract.version, + // publishDate, + // publisherEnsOrAddress, + // publisherProfile.data?.avatar, + // ], + // ); - const twitterIntentUrl = useMemo(() => { - const url = new URL("https://twitter.com/intent/tweet"); - url.searchParams.append( - "text", - `Check out this ${publishedContractName} contract on @thirdweb + // const twitterIntentUrl = useMemo(() => { + // const url = new URL("https://twitter.com/intent/tweet"); + // url.searchParams.append( + // "text", + // `Check out this ${publishedContractName} contract on @thirdweb -Deploy it in one click`, - ); - url.searchParams.append("url", currentRoute); - return url.href; - }, [publishedContractName, currentRoute]); + // Deploy it in one click`, + // ); + // url.searchParams.append("url", currentRoute); + // return url.href; + // }, [publishedContractName, currentRoute]); const sources = useQuery({ - queryKey: ["sources", contract], + queryKey: ["sources", publishedContract.publishMetadataUri], queryFn: async () => { invariant( - contractPublishMetadata.data?.compilerMetadata?.sources, + publishedContract.metadata.sources, "no compilerMetadata sources available", ); - return ( - await fetchSourceFilesFromMetadata( - { - metadata: { - sources: contractPublishMetadata.data.compilerMetadata.sources, - }, - } as unknown as PublishedMetadata, - StorageSingleton, - ) - ) + return (await fetchSourceFilesFromMetadata(publishedContract)) .map((source) => { return { ...source, @@ -208,44 +129,30 @@ Deploy it in one click`, .slice() .reverse(); }, - enabled: !!contractPublishMetadata.data?.compilerMetadata?.sources, + enabled: !!publishedContract.metadata.sources, }); - const title = useMemo(() => { - let clearType = ""; - if (extensionNames.includes("ERC721")) { - clearType = "ERC721"; - } else if (extensionNames.includes("ERC20")) { - clearType = "ERC20"; - } else if (extensionNames.includes("ERC1155")) { - clearType = "ERC1155"; - } - if (clearType) { - return `${publishedContractName} - ${clearType} | Published Smart Contract`; - } + // const title = useMemo(() => { + // let clearType = ""; + // if (extensionNames.includes("ERC721")) { + // clearType = "ERC721"; + // } else if (extensionNames.includes("ERC20")) { + // clearType = "ERC20"; + // } else if (extensionNames.includes("ERC1155")) { + // clearType = "ERC1155"; + // } + // if (clearType) { + // return `${publishedContractName} - ${clearType} | Published Smart Contract`; + // } - return `${publishedContractName} | Published Smart Contract`; - }, [extensionNames, publishedContractName]); + // return `${publishedContractName} | Published Smart Contract`; + // }, [extensionNames, publishedContractName]); - const implementationAddresses = useMemo( - () => - publishedContractInfo.data?.publishedMetadata?.factoryDeploymentData - ?.implementationAddresses, - [ - publishedContractInfo.data?.publishedMetadata?.factoryDeploymentData - ?.implementationAddresses, - ], - ); + const implementationAddresses = + publishedContract.factoryDeploymentData?.implementationAddresses; - const factoryAddresses = useMemo( - () => - publishedContractInfo.data?.publishedMetadata?.factoryDeploymentData - ?.factoryAddresses, - [ - publishedContractInfo.data?.publishedMetadata?.factoryDeploymentData - ?.factoryAddresses, - ], - ); + const factoryAddresses = + publishedContract.factoryDeploymentData?.factoryAddresses; const hasImplementationAddresses = useMemo( () => Object.values(implementationAddresses || {}).some((v) => v !== ""), @@ -259,7 +166,7 @@ Deploy it in one click`, return ( <> - + /> */} {/* Farcaster frames headers */} - + {/* - + */} - {address === contract.publisher && ( + {address === publishedContract.publisher && ( } href={`/contracts/publish/${encodeURIComponent( - contract.metadataUri.replace("ipfs://", ""), + publishedContract.publishMetadataUri.replace("ipfs://", ""), )}`} > Edit )} - {publishedContractInfo.data?.publishedMetadata?.readme && ( + {publishedContract?.readme && ( - + )} - {publishedContractInfo.data?.publishedMetadata?.changelog && ( + {publishedContract?.changelog && ( - {publishedContractInfo.data?.publishedMetadata?.version} Release - Notes + {publishedContract?.version} Release Notes @@ -327,22 +229,17 @@ Deploy it in one click`, px={6} pt={2} pb={5} - markdownText={ - publishedContractInfo.data?.publishedMetadata?.changelog - } + markdownText={publishedContract?.changelog} /> )} {contractFunctions && ( )} @@ -358,7 +255,7 @@ Deploy it in one click`, <> - {publishedContractInfo.data?.publishedTimestamp && ( + {publishedContract.publishTimestamp && ( @@ -373,7 +270,7 @@ Deploy it in one click`, )} - {publishedContractInfo.data?.publishedMetadata?.audit && ( + {publishedContract?.audit && ( @@ -383,10 +280,7 @@ Deploy it in one click`, - {(publishedContractInfo.data?.publishedMetadata - ?.isDeployableViaProxy && + {(publishedContract?.isDeployableViaProxy && hasImplementationAddresses) || - (publishedContractInfo.data?.publishedMetadata - ?.isDeployableViaFactory && + (publishedContract?.isDeployableViaFactory && hasFactoryAddresses) ? ( - {publishedContractInfo.data?.publishedMetadata - ?.isDeployableViaFactory + {publishedContract?.isDeployableViaFactory ? "Factory" : "Proxy"}{" "} Enabled @@ -445,8 +336,7 @@ Deploy it in one click`, ) : null} {factoryAddresses && hasFactoryAddresses && - publishedContractInfo.data?.publishedMetadata - ?.isDeployableViaFactory ? ( + publishedContract?.isDeployableViaFactory ? ( - {contractPublishMetadata.data?.abi && ( - - )} - - - - Share - - - - } - category="released-contract" - label="share-twitter" - /> - - - - Learn about Publish + Learn more about Publish @@ -524,3 +369,53 @@ Deploy it in one click`, ); }; + +// TODO: find a place to put this +export type ContractSource = { + filename: string; + source: string; +}; +async function fetchSourceFilesFromMetadata( + publishedMetadata: ExtendedPublishedContract, +): Promise { + return await Promise.all( + Object.entries(publishedMetadata.metadata.sources).map( + async ([path, info]) => { + const urls = "urls" in info ? info.urls : undefined; + const ipfsLink = urls + ? urls.find((url) => url.includes("ipfs")) + : undefined; + if (ipfsLink) { + const ipfsHash = ipfsLink.split("ipfs/")[1]; + // 3 sec timeout for sources that haven't been uploaded to ipfs + const timeout = new Promise((_resolve, reject) => + setTimeout(() => reject("timeout"), 3000), + ); + const source = await Promise.race([ + ( + await download({ + uri: `ipfs://${ipfsHash}`, + client: thirdwebClient, + }) + ).text(), + timeout, + ]); + return { + filename: path, + source, + }; + } + if ("content" in info) { + return { + filename: path, + source: info.content || "Could not find source for this contract", + }; + } + return { + filename: path, + source: "Could not find source for this contract", + }; + }, + ), + ); +} diff --git a/apps/dashboard/src/components/contract-functions/contract-function.tsx b/apps/dashboard/src/components/contract-functions/contract-function.tsx index f016d2caa..fd71334d9 100644 --- a/apps/dashboard/src/components/contract-functions/contract-function.tsx +++ b/apps/dashboard/src/components/contract-functions/contract-function.tsx @@ -20,14 +20,20 @@ import { Thead, Tr, } from "@chakra-ui/react"; -import type { Abi, AbiEvent, AbiFunction } from "abitype"; +import { + type Abi, + type AbiEvent, + type AbiFunction, + formatAbiItem, +} from "abitype"; import { useContractEnabledExtensions } from "components/contract-components/hooks"; import { camelToTitle } from "contract-ui/components/solidity-inputs/helpers"; -import { useRouter } from "next/router"; +import { usePathname, useSearchParams } from "next/navigation"; import { type Dispatch, type SetStateAction, useMemo, useState } from "react"; import type { ThirdwebContract } from "thirdweb"; import { toFunctionSelector } from "thirdweb/utils"; import { Badge, Button, Card, Heading, Text } from "tw-components"; +import { useDashboardRouter } from "../../@/lib/DashboardRouter"; import { COMMANDS, formatSnippet, @@ -66,13 +72,12 @@ function joinABIs(abis: Abi[], abiWithConstructor?: Abi): Abi { const flattenedAbis = filteredABIs.flat(); const finalABIs = unique(flattenedAbis, (a, b): boolean => { - return ( - "name" in a && - "name" in b && - a.name === b.name && - a.type === b.type && - a.inputs.length === b.inputs.length - ); + 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; @@ -326,8 +331,8 @@ export const ContractFunctionsPanel: React.FC = ({ }, [fnsOrEvents]); // Load state from the URL - const router = useRouter(); - const _selector = router.query.selector; + const searchParams = useSearchParams(); + const _selector = searchParams?.get("selector"); const _item = _selector ? fnsOrEvents.find((o) => { if (o.type === "function") { @@ -337,10 +342,10 @@ export const ContractFunctionsPanel: React.FC = ({ return null; }) : undefined; + const [selectedFunction, setSelectedFunction] = useState< AbiFunction | AbiEvent >(_item ?? fnsOrEvents[0]); - // Set the active tab to Write or Read depends on the `_item` const _defaultTabIndex = _item && @@ -479,7 +484,8 @@ const FunctionsOrEventsListItem: React.FC = ({ const isActive = selectedFunction?.name === fn.name && selectedFunction.inputs?.length === fn.inputs?.length; - const router = useRouter(); + const pathname = usePathname(); + const router = useDashboardRouter(); return ( - - -
    -
    -
    - {modulesList} -
    - - {/* Content */} -
    - {showLoading ? ( -
    - -
    - ) : ( - - )} -
    -
    - ); - } - - return ( - - {/* Header */} -
    -
    - {contractInfo} -
    - {versionSelector} - -
    -
    - {modulesList} -
    - - {/* Content */} - - - - - {publishedContract && ( - - )} - -
    - ); -}; diff --git a/apps/dashboard/src/components/share-buttom/index.tsx b/apps/dashboard/src/components/share-buttom/index.tsx deleted file mode 100644 index 733ef4f34..000000000 --- a/apps/dashboard/src/components/share-buttom/index.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { Icon, useClipboard } from "@chakra-ui/react"; -import { FiCheck, FiShare2 } from "react-icons/fi"; -import { toast } from "sonner"; -import { TrackedIconButton } from "tw-components"; - -export const ShareButton: React.FC = (props) => { - const currentUrl = typeof window !== "undefined" ? window.location.href : ""; - const url = props.url || currentUrl; - const shareData = { ...props, url }; - const { onCopy, hasCopied } = useClipboard(shareData.url); - - const onShareClick = async () => { - // if browser supports sharing use native sharing - if ( - "canShare" in navigator && - navigator.canShare && - navigator.canShare(shareData) - ) { - try { - await navigator.share(shareData); - return; - } catch (err) { - console.warn("failed to share", err); - } - } else { - onCopy(); - toast.info("URL copied to clipboard"); - } - }; - - return ( - - } - category="released-contract" - label="copy-url" - onClick={onShareClick} - /> - ); -}; diff --git a/apps/dashboard/src/contexts/custom-sdk-context.tsx b/apps/dashboard/src/contexts/custom-sdk-context.tsx index e9cfd9d37..7aea54bbd 100644 --- a/apps/dashboard/src/contexts/custom-sdk-context.tsx +++ b/apps/dashboard/src/contexts/custom-sdk-context.tsx @@ -30,7 +30,7 @@ const PLACEHOLDER_CHAIN: StoredChain = { testnet: false, }; -export const CustomSDKContext: ComponentWithChildren<{ +const CustomSDKContext: ComponentWithChildren<{ desiredChainId?: number; options?: SDKOptions; }> = ({ desiredChainId, options, children }) => { diff --git a/apps/dashboard/src/contexts/error-handler.tsx b/apps/dashboard/src/contexts/error-handler.tsx index 95115b60d..bdfd4665d 100644 --- a/apps/dashboard/src/contexts/error-handler.tsx +++ b/apps/dashboard/src/contexts/error-handler.tsx @@ -1,3 +1,5 @@ +"use client"; + import { CopyTextButton } from "@/components/ui/CopyTextButton"; import { Button } from "@/components/ui/button"; import { 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 449295b48..07b0b31b7 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 @@ -244,12 +244,12 @@ const WALLETS_SNIPPETS = [ link: "https://portal.thirdweb.com/references/wallets/latest/SmartWallet", supportedLanguages: { javascript: `import { defineChain } from "thirdweb"; -import { embeddedWallet, smartWallet } from "thirdweb/wallets"; +import { inAppWallet, smartWallet } from "thirdweb/wallets"; const chain = defineChain({{chainId}}); -// First, connect the personal wallet, which can be any wallet (metamask, embedded, etc.) -const personalWallet = embeddedWallet(); +// First, connect the personal wallet, which can be any wallet (metamask, in-app, etc.) +const personalWallet = inAppWallet(); const peronalAccount = await personalWallet.connect({ client, chain, @@ -618,12 +618,12 @@ export const CodeOverview: React.FC = ({ the{" "} - Wallet SDK + Connect SDK {" "} in your applications. This will ensure account contracts are deployed for your users only when they need it. diff --git a/apps/dashboard/src/contract-ui/tabs/manage/components/getModuleInstalledParams.ts b/apps/dashboard/src/contract-ui/tabs/manage/components/getModuleInstalledParams.ts index bc02beda5..4d77c1ad1 100644 --- a/apps/dashboard/src/contract-ui/tabs/manage/components/getModuleInstalledParams.ts +++ b/apps/dashboard/src/contract-ui/tabs/manage/components/getModuleInstalledParams.ts @@ -1,24 +1,21 @@ -import { extractFunctionParamsFromAbi } from "@thirdweb-dev/sdk"; -import { polygon } from "thirdweb/chains"; +import { thirdwebClient } from "@/constants/client"; +import { fetchPublishedContractVersions } from "components/contract-components/fetch-contracts-with-versions"; +import { isAddress } from "thirdweb"; +import { resolveAddress } from "thirdweb/extensions/ens"; import invariant from "tiny-invariant"; -import { - fetchAllVersions, - fetchContractPublishMetadataFromURI, -} from "../../../../components/contract-components/hooks"; -import { getDashboardChainRpc } from "../../../../lib/rpc"; -import { getThirdwebSDK } from "../../../../lib/sdk"; import type { ModuleMeta } from "./install-module-params"; export async function getModuleInstalledParams(ext: ModuleMeta) { - const sdk = getThirdwebSDK( - polygon.id, - getDashboardChainRpc(polygon.id, undefined), - ); - // get all versions of the module - const allPublishedModules = await fetchAllVersions( - sdk, - ext.publisherAddress, + // if the publisher is an ens name, resolve it + const publisherAddress = isAddress(ext.publisherAddress) + ? ext.publisherAddress + : await resolveAddress({ + client: thirdwebClient, + name: ext.publisherAddress, + }); + const allPublishedModules = await fetchPublishedContractVersions( + publisherAddress, ext.moduleName, ); @@ -33,18 +30,14 @@ export async function getModuleInstalledParams(ext: ModuleMeta) { `Module ${ext.moduleName} version "${ext.moduleVersion}" not found`, ); - // get the ABI - const contractMetadata = await fetchContractPublishMetadataFromURI( - publishedModule.metadataUri, - ); - - invariant(contractMetadata.abi, `ABI not found for module ${ext.moduleName}`); + invariant(publishedModule.abi, `ABI not found for module ${ext.moduleName}`); // get encodeBytesOnInstall function's install params - const installParamsForModule = extractFunctionParamsFromAbi( - contractMetadata.abi, - "encodeBytesOnInstall", - ); + + const installParamsForModule = + publishedModule.abi + .filter((f) => f.type === "function") + .find((f) => f.name === "encodeBytesOnInstall")?.inputs || []; return { moduleName: ext.moduleName, diff --git a/apps/dashboard/src/contract-ui/tabs/manage/components/install-module-params.tsx b/apps/dashboard/src/contract-ui/tabs/manage/components/install-module-params.tsx index 286e9558e..64fcb6beb 100644 --- a/apps/dashboard/src/contract-ui/tabs/manage/components/install-module-params.tsx +++ b/apps/dashboard/src/contract-ui/tabs/manage/components/install-module-params.tsx @@ -56,7 +56,10 @@ export function ModuleInstallParams(props: { {param.name} = ({ abi }) => { - const enabledExtensions = useContractEnabledExtensions(abi); - - return ( - - - Extensions - - - {enabledExtensions.length ? ( - enabledExtensions.map((extension) => ( - - - Extension detected - - - {extension.name} - - - - - )) - ) : ( - - - No extensions detected - - - )} - - - ); -}; diff --git a/apps/dashboard/src/core-ui/batch-upload/progress-box.tsx b/apps/dashboard/src/core-ui/batch-upload/progress-box.tsx deleted file mode 100644 index 19f7e6c40..000000000 --- a/apps/dashboard/src/core-ui/batch-upload/progress-box.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { Flex, Progress } from "@chakra-ui/react"; -import { useEffect, useState } from "react"; -import { Text } from "tw-components"; - -export type UploadProgressEvent = { - progress: number; - total: number; -}; - -interface ProgressBoxProps { - progress: UploadProgressEvent; -} - -export const ProgressBox: React.FC = ({ progress }) => { - const isFinished = progress.progress >= progress.total; - const [takingLong, setTakingLong] = useState(false); - - // legitimate use-case - // eslint-disable-next-line no-restricted-syntax - useEffect(() => { - if (isFinished) { - const t = setTimeout(() => { - setTakingLong(true); - }, 10000); - - return () => { - clearTimeout(t); - }; - } - }, [isFinished]); - - return ( - - - {takingLong && progress.progress !== 0 && ( - - This may take a while. - - )} - - ); -}; diff --git a/apps/dashboard/src/hooks/useQueryParam.ts b/apps/dashboard/src/hooks/useQueryParam.ts index 001520bce..8dd2956eb 100644 --- a/apps/dashboard/src/hooks/useQueryParam.ts +++ b/apps/dashboard/src/hooks/useQueryParam.ts @@ -1,10 +1,13 @@ -import { useRouter } from "next/router"; -import { getSingleQueryValue } from "utils/router"; +import { useSearchParams } from "next/navigation"; +import {} from "next/router"; +/** + * @deprecated use `useSearchParams` from `next/navigation` instead + */ export function useSingleQueryParam( key: string, ): T | undefined { - const { query } = useRouter(); + const searchParams = useSearchParams(); - return getSingleQueryValue(query, key) as T; + return searchParams?.get(key) as T | undefined; } diff --git a/apps/dashboard/src/lib/vercel-utils.ts b/apps/dashboard/src/lib/vercel-utils.ts index 4aa9252d1..b49635939 100644 --- a/apps/dashboard/src/lib/vercel-utils.ts +++ b/apps/dashboard/src/lib/vercel-utils.ts @@ -46,7 +46,7 @@ export function getAbsoluteUrl(): string { return "https://thirdweb.com"; } -function setOverrides() { +export function setOverrides() { if (getVercelEnv() === "production") { // no overrides return; diff --git a/apps/dashboard/src/middleware.ts b/apps/dashboard/src/middleware.ts index 2c23e2c71..d75d0e34d 100644 --- a/apps/dashboard/src/middleware.ts +++ b/apps/dashboard/src/middleware.ts @@ -88,7 +88,7 @@ export async function middleware(request: NextRequest) { } // if we have more than 1 path part, we're in the
    / case -> publish page if (paths.length > 1) { - return rewrite(request, `/publish${pathname}`); + return rewrite(request, `/published-contract${pathname}`); } } // END /
    /... case diff --git a/apps/dashboard/src/og-lib/url-utils.ts b/apps/dashboard/src/og-lib/url-utils.ts index 860fca1f9..b580ed5c7 100644 --- a/apps/dashboard/src/og-lib/url-utils.ts +++ b/apps/dashboard/src/og-lib/url-utils.ts @@ -101,13 +101,6 @@ function fromUrl(type: keyof OgProps, url: URL): OgProps[typeof type] { } } -export const PublishedContractOG = { - toUrl: (props: OgProps["publishedContract"]) => - toUrl("publishedContract", props), - fromUrl: (url: URL) => - fromUrl("publishedContract", url) as OgProps["publishedContract"], -}; - export const ProfileOG = { toUrl: (props: OgProps["profile"]) => toUrl("profile", props), fromUrl: (url: URL) => fromUrl("profile", url) as OgProps["profile"], diff --git a/apps/dashboard/src/page-id.ts b/apps/dashboard/src/page-id.ts index 869b59ba4..c8d3647c8 100644 --- a/apps/dashboard/src/page-id.ts +++ b/apps/dashboard/src/page-id.ts @@ -174,19 +174,12 @@ export enum PageId { // example: thirdweb.com/jns.eth Profile = "profile", - // thirdweb.com/:wallet/:contractId - // example: thirdweb.com/jns.eth/PermissionedERC721A - PublishedContract = "published-contract", - // --------------------------------------------------------------------------- // "deploy" product pages // --------------------------------------------------------------------------- // thirdweb.com/contracts/deploy DeployMultiple = "deploy-multiple-contracts", - // thirdweb.com/contracts/deploy/:id - DeploySingle = "deploy-single-contract", - // thirdweb.com/:network/:contractAddress (evm) // example: thirdweb.com/goerli/0x2eaDAa60dBB74Ead3E20b23E4C5A0Dd789932846 DeployedContract = "deployed-contract", diff --git a/apps/dashboard/src/pages/contracts/deploy/[contractId].tsx b/apps/dashboard/src/pages/contracts/deploy/[contractId].tsx deleted file mode 100644 index 725f0c5b8..000000000 --- a/apps/dashboard/src/pages/contracts/deploy/[contractId].tsx +++ /dev/null @@ -1,199 +0,0 @@ -import { - Box, - Center, - Divider, - Flex, - Icon, - IconButton, - Image, - LinkBox, - LinkOverlay, - Skeleton, -} from "@chakra-ui/react"; -import { AppLayout } from "components/app-layouts/app"; -import { ContractDeployForm } from "components/contract-components/contract-deploy-form"; -import { - useContractEnabledExtensions, - useContractPublishMetadataFromURI, -} from "components/contract-components/hooks"; -import { ContractIdImage } from "components/contract-components/shared/contract-id-image"; -import { useTrack } from "hooks/analytics/useTrack"; -import { useSingleQueryParam } from "hooks/useQueryParam"; -import { useRouter } from "next/router"; -import { PageId } from "page-id"; -import { useEffect, useRef } from "react"; -import { FiArrowLeft, FiExternalLink } from "react-icons/fi"; -import { Card, Heading, LinkButton, Text, TrackedLink } from "tw-components"; -import type { ThirdwebNextPage } from "utils/types"; - -const ContractDeployDetailPage: ThirdwebNextPage = () => { - const router = useRouter(); - const trackEvent = useTrack(); - const contractId = useSingleQueryParam("contractId"); - const from = useSingleQueryParam("from"); - const publishMetadataQuery = useContractPublishMetadataFromURI( - contractId || "", - ); - - const hasTrackedImpression = useRef(false); - // TODO: find better way to do impression tracking - // eslint-disable-next-line no-restricted-syntax - useEffect(() => { - if (publishMetadataQuery.data && !hasTrackedImpression.current) { - hasTrackedImpression.current = true; - trackEvent({ - action: "impression", - category: "deploy", - analytics: publishMetadataQuery.data.analytics, - }); - } - }, [publishMetadataQuery.data, trackEvent]); - - const enabledFeatures = useContractEnabledExtensions( - // @ts-expect-error - "wrong" ABI data type - publishMetadataQuery.data?.abi, - ); - - return ( - <> - - - - {from && ( - router.back()} - icon={} - /> - )} - {contractId && ( - - )} - - - - {publishMetadataQuery.isSuccess - ? publishMetadataQuery.data?.name - : "testing testing testing"} - - - - - - - - {contractId && ( - - - - {enabledFeatures.length > 0 && ( - - - - - - Detected Extensions - - - - - - These extensions will automatically be available for - this contract in the dashboard as well as in the SDKs. - - - - {enabledFeatures.map((feature) => ( - - ))} - - - )} - - - - - )} -
    - - Learn about thirdweb extensions - -
    -
    -
    - - ); -}; - -// const AppLayout = dynamic( -// async () => (await import("components/app-layouts/app")).AppLayout, -// ); - -ContractDeployDetailPage.getLayout = function getLayout(page, props) { - return {page}; -}; - -ContractDeployDetailPage.pageId = PageId.DeploySingle; - -export default ContractDeployDetailPage; - -// biome-ignore lint/suspicious/noExplicitAny: This will be removed once we fully migrate to v5 -type FeatureWithEnabled = any & { - features: Record; - enabled: boolean; -}; -interface EnabledFeatureProps { - feature: FeatureWithEnabled; -} - -const EnabledFeature: React.FC = ({ feature }) => { - const trackEvent = useTrack(); - return ( - - - - - - trackEvent({ - category: "extensions-deploy", - action: "click", - label: feature.name, - }) - } - > - - {feature.name} - - - - - - - ); -}; diff --git a/apps/dashboard/src/pages/publish/[...paths].tsx b/apps/dashboard/src/pages/publish/[...paths].tsx deleted file mode 100644 index fcf3c2c52..000000000 --- a/apps/dashboard/src/pages/publish/[...paths].tsx +++ /dev/null @@ -1,173 +0,0 @@ -import { Flex, Spinner } from "@chakra-ui/react"; -import { - type DehydratedState, - QueryClient, - dehydrate, -} from "@tanstack/react-query"; -import { AppLayout } from "components/app-layouts/app"; -import { - ensQuery, - fetchAllVersions, - fetchContractPublishMetadataFromURI, - fetchPublishedContractInfo, - publisherProfileQuery, -} from "components/contract-components/hooks"; -import { - PublishWithVersionPage, - type PublishWithVersionPageProps, -} from "components/pages/publish"; -import { PublisherSDKContext } from "contexts/custom-sdk-context"; -import { getDashboardChainRpc } from "lib/rpc"; -import { getThirdwebSDK } from "lib/sdk"; -import type { GetStaticPaths, GetStaticProps } from "next"; -import { PageId } from "page-id"; -import { polygon } from "thirdweb/chains"; -import { getAddress, isAddress } from "thirdweb/utils"; -import type { ThirdwebNextPage } from "utils/types"; -import { ContractsSidebarLayout } from "../../core-ui/sidebar/contracts"; - -type PublishPageProps = { - dehydratedState: DehydratedState; -} & PublishWithVersionPageProps; - -const PublishPage: ThirdwebNextPage = (props: PublishPageProps) => { - return ( - - - - ); -}; - -PublishPage.pageId = PageId.PublishedContract; - -PublishPage.getLayout = (page, props: PublishPageProps) => { - return ( - - - {page} - - - ); -}; - -PublishPage.fallback = ( - - - - - -); - -export default PublishPage; - -export const getStaticProps: GetStaticProps = async (ctx) => { - const paths = ctx.params?.paths as string[]; - const [authorAddress, contractName, versionOrDeploy = "", deployStr] = paths; - let version = ""; - let isDeploy = false; - - if (versionOrDeploy === "deploy") { - isDeploy = true; - version = ""; - } else if (versionOrDeploy && deployStr === "deploy") { - version = versionOrDeploy; - isDeploy = true; - } - - if (!contractName) { - return { - notFound: true, - }; - } - - const polygonSdk = getThirdwebSDK( - polygon.id, - getDashboardChainRpc(polygon.id, undefined), - ); - - const lowercaseAddress = authorAddress.toLowerCase(); - const checksummedAddress = isAddress(lowercaseAddress) - ? getAddress(lowercaseAddress) - : lowercaseAddress; - - const queryClient = new QueryClient(); - const { address, ensName } = await queryClient.fetchQuery( - ensQuery(checksummedAddress), - ); - - if (!address) { - return { - notFound: true, - } as const; - } - - // TODO get the latest version instead of all versions - // OR wait till contract upgrade to have a faster call for this - - let allVersions: ReturnType extends Promise - ? X - : never = []; - try { - allVersions = await queryClient.fetchQuery( - ["all-releases", address, contractName], - () => fetchAllVersions(polygonSdk, address, contractName), - ); - } catch { - return { - notFound: true, - } as const; - } - - const publishedContract = - allVersions.find((v) => v.version === version) || allVersions[0]; - - const ensQueries = [queryClient.prefetchQuery(ensQuery(checksummedAddress))]; - if (ensName) { - ensQueries.push(queryClient.prefetchQuery(ensQuery(ensName))); - } - - // this can be very slow - await Promise.all([ - ...ensQueries, - queryClient.prefetchQuery(["released-contract", publishedContract], () => - fetchPublishedContractInfo(polygonSdk, publishedContract), - ), - queryClient.prefetchQuery( - ["publish-metadata", publishedContract.metadataUri], - () => fetchContractPublishMetadataFromURI(publishedContract.metadataUri), - ), - queryClient.prefetchQuery( - publisherProfileQuery(publishedContract.publisher), - ), - ]); - - const props: PublishPageProps = { - dehydratedState: dehydrate(queryClient), - author: checksummedAddress, - contractName, - version, - isDeploy, - }; - - return { - props, - }; -}; - -// generate the explore contracts at build time -// others will be generated at runtime via fallback -export const getStaticPaths: GetStaticPaths = async () => { - return { - fallback: true, - paths: [], - }; -}; diff --git a/apps/dashboard/src/utils/router.ts b/apps/dashboard/src/utils/router.ts index 2617fd4be..c863c80c1 100644 --- a/apps/dashboard/src/utils/router.ts +++ b/apps/dashboard/src/utils/router.ts @@ -1,5 +1,8 @@ import type { ParsedUrlQuery } from "node:querystring"; +/** + * @deprecated use `SearchParams` instead + */ export function getSingleQueryValue( query: ParsedUrlQuery, key: string, diff --git a/apps/playground-web/src/components/social/social-profiles.tsx b/apps/playground-web/src/components/social/social-profiles.tsx index 6086051fe..1e65159ad 100644 --- a/apps/playground-web/src/components/social/social-profiles.tsx +++ b/apps/playground-web/src/components/social/social-profiles.tsx @@ -102,6 +102,7 @@ function SocialProfileCard({ profile }: { profile: SocialProfile }) { return (
    {avatar ? ( + // eslint-disable-next-line @next/next/no-img-element {profile.name} ) : (
    diff --git a/packages/thirdweb/package.json b/packages/thirdweb/package.json index 00f468e48..c1aeac451 100644 --- a/packages/thirdweb/package.json +++ b/packages/thirdweb/package.json @@ -305,7 +305,7 @@ "dev": "tsc --project ./tsconfig.build.json --module es2020 --outDir ./dist/esm --watch", "dev:cjs": "printf '{\"type\":\"commonjs\"}' > ./dist/cjs/package.json && tsc --noCheck --project ./tsconfig.build.json --module commonjs --outDir ./dist/cjs --verbatimModuleSyntax false --watch", "dev:esm": "printf '{\"type\": \"module\",\"sideEffects\":false}' > ./dist/esm/package.json && tsc --noCheck --project ./tsconfig.build.json --module es2020 --outDir ./dist/esm --watch", - "build": "pnpm clean && pnpm build:cjs && pnpm build:esm && pnpm build:types", + "build": "pnpm clean && pnpm build:types && pnpm build:cjs && pnpm build:esm", "build:cjs": "tsc --noCheck --project ./tsconfig.build.json --module commonjs --outDir ./dist/cjs --verbatimModuleSyntax false && printf '{\"type\":\"commonjs\"}' > ./dist/cjs/package.json", "build:esm": "tsc --noCheck --project ./tsconfig.build.json --module es2020 --outDir ./dist/esm && printf '{\"type\": \"module\",\"sideEffects\":false}' > ./dist/esm/package.json", "build:types": "tsc --project ./tsconfig.build.json --module esnext --declarationDir ./dist/types --emitDeclarationOnly --declaration --declarationMap", diff --git a/packages/thirdweb/src/contract/deployment/deploy-with-abi.ts b/packages/thirdweb/src/contract/deployment/deploy-with-abi.ts index 108babd06..061a13baa 100644 --- a/packages/thirdweb/src/contract/deployment/deploy-with-abi.ts +++ b/packages/thirdweb/src/contract/deployment/deploy-with-abi.ts @@ -5,12 +5,14 @@ import { encodeAbiParameters } from "../../utils/abi/encodeAbiParameters.js"; import { normalizeFunctionParams } from "../../utils/abi/normalizeFunctionParams.js"; import { computeDeploymentAddress } from "../../utils/any-evm/compute-deployment-address.js"; import { computeDeploymentInfoFromBytecode } from "../../utils/any-evm/compute-published-contract-deploy-info.js"; +import { isContractDeployed } from "../../utils/bytecode/is-contract-deployed.js"; import { ensureBytecodePrefix } from "../../utils/bytecode/prefix.js"; import { concatHex } from "../../utils/encoding/helpers/concat-hex.js"; import { type Hex, isHex } from "../../utils/encoding/hex.js"; import type { Prettify } from "../../utils/type-utils.js"; import type { ClientAndChain } from "../../utils/types.js"; import type { Account } from "../../wallets/interfaces/wallet.js"; +import { getContract } from "../contract.js"; /** * @extension DEPLOY @@ -122,6 +124,22 @@ export async function deployContract( if (options.salt !== undefined) { // Deploy with CREATE2 if salt is provided const info = await computeDeploymentInfoFromBytecode(options); + const address = computeDeploymentAddress({ + bytecode: options.bytecode, + encodedArgs: info.encodedArgs, + create2FactoryAddress: info.create2FactoryAddress, + salt: options.salt, + }); + const isDeployed = await isContractDeployed( + getContract({ + client: options.client, + chain: options.chain, + address, + }), + ); + if (isDeployed) { + throw new Error(`Contract already deployed at address: ${address}`); + } await sendAndConfirmTransaction({ account: options.account, transaction: prepareTransaction({ @@ -131,12 +149,7 @@ export async function deployContract( data: info.initBytecodeWithsalt, }), }); - return computeDeploymentAddress({ - bytecode: options.bytecode, - encodedArgs: info.encodedArgs, - create2FactoryAddress: info.create2FactoryAddress, - salt: options.salt, - }); + return address; } const deployTx = prepareDirectDeployTransaction(options); diff --git a/packages/thirdweb/src/exports/deploys.ts b/packages/thirdweb/src/exports/deploys.ts index b696496a3..a867ed235 100644 --- a/packages/thirdweb/src/exports/deploys.ts +++ b/packages/thirdweb/src/exports/deploys.ts @@ -46,4 +46,4 @@ export { type PrepareDirectDeployTransactionOptions, } from "../contract/deployment/deploy-with-abi.js"; export { computePublishedContractAddress } from "../utils/any-evm/compute-published-contract-address.js"; -export { getRequiredTransactionCount } from "../extensions/prebuilts/get-required-transactions.js"; +export { getRequiredTransactions } from "../extensions/prebuilts/get-required-transactions.js"; diff --git a/packages/thirdweb/src/extensions/prebuilts/deploy-published.ts b/packages/thirdweb/src/extensions/prebuilts/deploy-published.ts index afb7cd2b2..afcdfb5be 100644 --- a/packages/thirdweb/src/extensions/prebuilts/deploy-published.ts +++ b/packages/thirdweb/src/extensions/prebuilts/deploy-published.ts @@ -18,6 +18,7 @@ import type { FetchDeployMetadataResult } from "../../utils/any-evm/deploy-metad import { isZkSyncChain } from "../../utils/any-evm/zksync/isZkSyncChain.js"; import type { Hex } from "../../utils/encoding/hex.js"; import type { Account } from "../../wallets/interfaces/wallet.js"; +import { getAllDefaultConstructorParamsForImplementation } from "./get-required-transactions.js"; /** * @extension DEPLOY @@ -168,7 +169,12 @@ export async function deployContractfromDeployMetadata( client, account, contractId: deployMetadata.name, - constructorParams: implementationConstructorParams, + constructorParams: + implementationConstructorParams || + (await getAllDefaultConstructorParamsForImplementation({ + chain, + client, + })), publisher: deployMetadata.publisher, }); diff --git a/packages/thirdweb/src/extensions/prebuilts/get-required-transactions.test.ts b/packages/thirdweb/src/extensions/prebuilts/get-required-transactions.test.ts index ad0832321..1d448bc7a 100644 --- a/packages/thirdweb/src/extensions/prebuilts/get-required-transactions.test.ts +++ b/packages/thirdweb/src/extensions/prebuilts/get-required-transactions.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest"; import { CLEAN_ANVIL_CHAIN } from "../../../test/src/chains.js"; import { TEST_CLIENT } from "../../../test/src/test-clients.js"; import { fetchPublishedContractMetadata } from "../../contract/deployment/publisher.js"; -import { getRequiredTransactionCount } from "./get-required-transactions.js"; +import { getRequiredTransactions } from "./get-required-transactions.js"; describe.runIf(process.env.TW_SECRET_KEY)( "getRequiredTransactions", @@ -11,47 +11,49 @@ describe.runIf(process.env.TW_SECRET_KEY)( }, () => { it("should count transactions for a published direct deploy contract", async () => { - const metadata = await fetchPublishedContractMetadata({ + const deployMetadata = await fetchPublishedContractMetadata({ client: TEST_CLIENT, contractId: "AccountFactory", }); - const results = await getRequiredTransactionCount({ + const results = await getRequiredTransactions({ client: TEST_CLIENT, chain: CLEAN_ANVIL_CHAIN, - metadata, + deployMetadata, }); expect(results.length).toBe(1); }); it("should count transactions for a published autofactory contract", async () => { - const metadata = await fetchPublishedContractMetadata({ + const deployMetadata = await fetchPublishedContractMetadata({ client: TEST_CLIENT, contractId: "Airdrop", }); - const results = await getRequiredTransactionCount({ + const results = await getRequiredTransactions({ client: TEST_CLIENT, chain: CLEAN_ANVIL_CHAIN, - metadata, + deployMetadata, }); expect(results.length).toBe(4); }); it("should count transactions for a modular contract with modules", async () => { - const metadata = await fetchPublishedContractMetadata({ + const deployMetadata = await fetchPublishedContractMetadata({ client: TEST_CLIENT, contractId: "ERC721CoreInitializable", }); - const results = await getRequiredTransactionCount({ + const results = await getRequiredTransactions({ client: TEST_CLIENT, chain: CLEAN_ANVIL_CHAIN, - metadata, + deployMetadata, modules: await Promise.all( ["ClaimableERC721", "BatchMetadataERC721", "RoyaltyERC721"].map( (contractId) => fetchPublishedContractMetadata({ client: TEST_CLIENT, contractId, - }), + }).then((m) => ({ + deployMetadata: m, + })), ), ), }); diff --git a/packages/thirdweb/src/extensions/prebuilts/get-required-transactions.ts b/packages/thirdweb/src/extensions/prebuilts/get-required-transactions.ts index ccaa3c117..c34368792 100644 --- a/packages/thirdweb/src/extensions/prebuilts/get-required-transactions.ts +++ b/packages/thirdweb/src/extensions/prebuilts/get-required-transactions.ts @@ -5,6 +5,8 @@ import { getDeployedInfraContract } from "../../contract/deployment/utils/infra. import { getDeployedInfraContractFromMetadata } from "../../contract/deployment/utils/infra.js"; import { computePublishedContractAddress } from "../../utils/any-evm/compute-published-contract-address.js"; import type { FetchDeployMetadataResult } from "../../utils/any-evm/deploy-metadata.js"; +import { isZkSyncChain } from "../../utils/any-evm/zksync/isZkSyncChain.js"; +import type { DeployContractfromDeployMetadataOptions } from "./deploy-published.js"; /** * @internal @@ -13,6 +15,7 @@ export type DeployTransactionType = | "infra" | "implementation" | "module" + | "extension" | "proxy"; /** @@ -26,36 +29,42 @@ export type DeployTransactionResult = { /** * @internal */ -export async function getRequiredTransactionCount(options: { - chain: Chain; - client: ThirdwebClient; - metadata: FetchDeployMetadataResult; - constructorParams?: Record; - implementationConstructorParams?: Record; - modules?: FetchDeployMetadataResult[]; -}): Promise { +export async function getRequiredTransactions( + options: Omit< + DeployContractfromDeployMetadataOptions, + "account" | "initializeParams" + >, +): Promise { const { chain, client, - metadata, + deployMetadata, implementationConstructorParams, modules = [], } = options; - if (metadata?.deployType === "autoFactory") { - const results: (DeployTransactionResult | null)[] = await Promise.all([ + if (deployMetadata?.deployType === "autoFactory") { + const results: ( + | DeployTransactionResult + | DeployTransactionResult[] + | null + )[] = await Promise.all([ getDeployedCreate2Factory({ chain, client, }).then((c) => - c ? null : ({ type: "infra", contractId: "Create2Factory" } as const), + c || isZkSyncChain(chain) + ? null + : ({ type: "infra", contractId: "Create2Factory" } as const), ), getDeployedInfraContract({ chain, client, contractId: "Forwarder", }).then((c) => - c ? null : ({ type: "infra", contractId: "Forwarder" } as const), + c || isZkSyncChain(chain) + ? null + : ({ type: "infra", contractId: "Forwarder" } as const), ), getDeployedInfraContract({ chain, @@ -69,42 +78,150 @@ export async function getRequiredTransactionCount(options: { }), }, }).then((c) => - c ? null : ({ type: "infra", contractId: "TWCloneFactory" } as const), + c || isZkSyncChain(chain) + ? null + : ({ type: "infra", contractId: "TWCloneFactory" } as const), ), // TODO (deploy): add WETH contract check for implementations that need it (check implementation constructor params) - getDeployedInfraContract({ + getTransactionsForImplementation({ chain, client, - contractId: metadata.name, - constructorParams: implementationConstructorParams, - publisher: metadata.publisher, - version: metadata.version, - }).then((c) => - c - ? null - : ({ - type: "implementation", - contractId: metadata.name, - } as const), - ), + deployMetadata, + implementationConstructorParams, + }), ...modules.map((m) => getDeployedInfraContractFromMetadata({ chain, client, - contractMetadata: m, + contractMetadata: m.deployMetadata, }).then((c) => c ? null : ({ type: "module", - contractId: m.name, + contractId: m.deployMetadata.name, } as const), ), ), ]); - results.push({ type: "proxy", contractId: metadata.name }); - return results.filter((r) => r !== null); + results.push({ type: "proxy", contractId: deployMetadata.name }); + return results.flat().filter((r) => r !== null); } - return [{ type: "implementation", contractId: metadata.name }]; + return [{ type: "implementation", contractId: deployMetadata.name }]; +} + +async function getTransactionsForImplementation(options: { + chain: Chain; + client: ThirdwebClient; + deployMetadata: FetchDeployMetadataResult; + implementationConstructorParams?: Record; +}): Promise { + const { chain, client, deployMetadata, implementationConstructorParams } = + options; + + if (deployMetadata.name === "MarketplaceV3") { + return getTransactionsForMaketplaceV3(options); + } + + const constructorParams = + implementationConstructorParams ?? + (await getAllDefaultConstructorParamsForImplementation({ + chain, + client, + })); + + const result = await getDeployedInfraContract({ + chain, + client, + contractId: deployMetadata.name, + constructorParams, + publisher: deployMetadata.publisher, + version: deployMetadata.version, + }).then((c) => + c + ? null + : ({ + type: "implementation", + contractId: deployMetadata.name, + } as const), + ); + return result ? [result] : []; +} + +async function getTransactionsForMaketplaceV3(options: { + chain: Chain; + client: ThirdwebClient; +}): Promise { + const { chain, client } = options; + const WETHAdress = await computePublishedContractAddress({ + chain, + client, + contractId: "WETH9", + }); + const extensions: (DeployTransactionResult | null)[] = await Promise.all([ + getDeployedInfraContract({ + chain, + client, + contractId: "WETH9", + }).then((c) => + c ? null : ({ type: "infra", contractId: "WETH9" } as const), + ), + getDeployedInfraContract({ + chain, + client, + contractId: "DirectListingsLogic", + constructorParams: { _nativeTokenWrapper: WETHAdress }, + }).then((c) => + c + ? null + : ({ type: "extension", contractId: "DirectListingsLogic" } as const), + ), + getDeployedInfraContract({ + chain, + client, + contractId: "EnglishAuctionsLogic", + constructorParams: { _nativeTokenWrapper: WETHAdress }, + }).then((c) => + c + ? null + : ({ type: "extension", contractId: "EnglishAuctionsLogic" } as const), + ), + getDeployedInfraContract({ + chain, + client, + contractId: "OffersLogic", + }).then((c) => + c ? null : ({ type: "extension", contractId: "OffersLogic" } as const), + ), + ]); + // hacky assumption: if we need to deploy any of the extensions, we also need to deploy the implementation + const transactions = extensions.filter((e) => e !== null); + if (transactions.length) { + transactions.push({ type: "implementation", contractId: "MarketplaceV3" }); + } + return transactions; +} + +export async function getAllDefaultConstructorParamsForImplementation(args: { + chain: Chain; + client: ThirdwebClient; +}) { + const { chain, client } = args; + const [forwarder, weth] = await Promise.all([ + computePublishedContractAddress({ + chain, + client, + contractId: "Forwarder", + }), + computePublishedContractAddress({ + chain, + client, + contractId: "WETH9", + }), + ]); + return { + trustedForwarder: forwarder, + nativeTokenWrapper: weth, + }; } diff --git a/packages/thirdweb/src/gas/fee-data.ts b/packages/thirdweb/src/gas/fee-data.ts index 1cb66c19a..bffb5b365 100644 --- a/packages/thirdweb/src/gas/fee-data.ts +++ b/packages/thirdweb/src/gas/fee-data.ts @@ -138,6 +138,7 @@ export async function getDefaultGasOverrides( async function getDynamicFeeData( client: ThirdwebClient, chain: Chain, + percentMultiplier = 10, ): Promise { let maxFeePerGas: null | bigint = null; let maxPriorityFeePerGas_: null | bigint = null; @@ -173,7 +174,10 @@ async function getDynamicFeeData( } // add 10% tip to maxPriorityFeePerGas for faster processing - maxPriorityFeePerGas_ = getPreferredPriorityFee(maxPriorityFeePerGas_); + maxPriorityFeePerGas_ = getPreferredPriorityFee( + maxPriorityFeePerGas_, + percentMultiplier, + ); // eip-1559 formula, doubling the base fee ensures that the tx can be included in the next 6 blocks no matter how busy the network is // good article on the subject: https://www.blocknative.com/blog/eip-1559-fees maxFeePerGas = baseBlockFee * 2n + maxPriorityFeePerGas_; diff --git a/packages/thirdweb/src/utils/abi/normalizeFunctionParams.ts b/packages/thirdweb/src/utils/abi/normalizeFunctionParams.ts index 2804849aa..92fb86e1c 100644 --- a/packages/thirdweb/src/utils/abi/normalizeFunctionParams.ts +++ b/packages/thirdweb/src/utils/abi/normalizeFunctionParams.ts @@ -1,4 +1,5 @@ import type { AbiConstructor, AbiFunction } from "abitype"; +import { parseAbiParams } from "../contract/parse-abi-params.js"; export function normalizeFunctionParams( abiFunction: AbiFunction | AbiConstructor | undefined, @@ -7,23 +8,27 @@ export function normalizeFunctionParams( if (!abiFunction) { return []; } - if (abiFunction.inputs?.length !== Object.keys(params).length) { - throw new Error( - `Expected ${abiFunction.inputs?.length || 0} parameters, but got ${Object.keys(params).length}`, - ); - } - return abiFunction.inputs.map((input, index) => { - const value = input.name; - if (value === undefined) { - throw new Error( - `Missing named parameter for ${"name" in abiFunction ? abiFunction.name : "constructor"} at index ${index}`, - ); - } - const valueWithoutUnderscore = value.replace(/^_+/, ""); - const normalizedValue = params[value] ?? params[valueWithoutUnderscore]; // handle _name, _symbol, etc passed without underscore in the keys - if (normalizedValue === undefined) { - throw new Error(`Missing value for parameter ${value} at index ${index}`); - } - return normalizedValue; - }); + + return parseAbiParams( + abiFunction.inputs.map((i) => i.type), + abiFunction.inputs.map((input, index) => { + const value = input.name; + if (value === undefined) { + throw new Error( + `Missing named parameter for ${"name" in abiFunction ? abiFunction.name : "constructor"} at index ${index}`, + ); + } + const valueWithoutUnderscore = value.replace(/^_+/, ""); + const normalizedValue = + valueWithoutUnderscore in params + ? params[valueWithoutUnderscore] + : params[value]; // handle _name, _symbol, etc passed without underscore in the keys + if (normalizedValue === undefined) { + throw new Error( + `Missing value for parameter ${value} at index ${index}`, + ); + } + return normalizedValue; + }), + ); } diff --git a/packages/thirdweb/src/utils/any-evm/deploy-metadata.ts b/packages/thirdweb/src/utils/any-evm/deploy-metadata.ts index 9bf8adf93..bcb1b65a1 100644 --- a/packages/thirdweb/src/utils/any-evm/deploy-metadata.ts +++ b/packages/thirdweb/src/utils/any-evm/deploy-metadata.ts @@ -48,6 +48,7 @@ export async function fetchDeployMetadata( return { ...rawMeta, ...parsedMeta, + version: rawMeta.version, bytecode: deployBytecode, }; } @@ -69,7 +70,7 @@ async function fetchAndParseCompilerMetadata( `Could not resolve metadata for contract at ${options.uri}`, ); } - return formatCompilerMetadata(metadata); + return { ...metadata, ...formatCompilerMetadata(metadata) }; } // types @@ -87,7 +88,37 @@ type RawCompilerMetadata = { type ParsedCompilerMetadata = { name: string; abi: Abi; - metadata: Record; + metadata: { + compiler: { + version: string; + }; + language: string; + output: { + abi: Abi; + devdoc: Record; + userdoc: Record; + }; + settings: { + compilationTarget: Record; + evmVersion: string; + libraries: Record; + optimizer: Record; + remappings: string[]; + }; + sources: Record< + string, + { keccak256: string } & ( + | { + content: string; + } + | { + urls: string[]; + license?: string; + } + ) + >; + [key: string]: unknown; + }; info: { title?: string; author?: string; diff --git a/packages/thirdweb/src/utils/any-evm/get-init-bytecode-with-salt.ts b/packages/thirdweb/src/utils/any-evm/get-init-bytecode-with-salt.ts index 8d873bdef..1e2f76a63 100644 --- a/packages/thirdweb/src/utils/any-evm/get-init-bytecode-with-salt.ts +++ b/packages/thirdweb/src/utils/any-evm/get-init-bytecode-with-salt.ts @@ -1,6 +1,6 @@ -import { type Hex, encodePacked } from "viem"; +import { encodePacked } from "viem/utils"; import { ensureBytecodePrefix } from "../bytecode/prefix.js"; -import { uint8ArrayToHex } from "../encoding/hex.js"; +import { type Hex, uint8ArrayToHex } from "../encoding/hex.js"; import { getSaltHash } from "./get-salt-hash.js"; import { keccakId } from "./keccak-id.js"; diff --git a/packages/thirdweb/src/utils/contract/parse-abi-params.ts b/packages/thirdweb/src/utils/contract/parse-abi-params.ts index 7cf9b406e..6784867cf 100644 --- a/packages/thirdweb/src/utils/contract/parse-abi-params.ts +++ b/packages/thirdweb/src/utils/contract/parse-abi-params.ts @@ -79,6 +79,9 @@ export function parseAbiParams( return value; } if (type.startsWith("uint") || type.startsWith("int")) { + if (typeof value === "bigint") { + return value; + } if (typeof value !== "string" && typeof value !== "number") { throw new Error(`Cannot convert type ${typeof value} to BigInt`); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 692b01f84..c41de2007 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2222,7 +2222,7 @@ importers: version: 3.592.0 '@aws-sdk/credential-providers': specifier: 3.592.0 - version: 3.592.0(@aws-sdk/client-sso-oidc@3.645.0(@aws-sdk/client-sts@3.645.0)) + version: 3.592.0(@aws-sdk/client-sso-oidc@3.645.0(@aws-sdk/client-sts@3.592.0)) '@coinbase/wallet-mobile-sdk': specifier: ^1 version: 1.0.13(expo@51.0.32(@babel/core@7.25.2)(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@5.0.10))(react-native@0.75.2(@babel/core@7.25.2)(@babel/preset-env@7.25.4(@babel/core@7.25.2))(@types/react@18.3.5)(bufferutil@4.0.8)(react@18.3.1)(typescript@5.5.4)(utf-8-validate@5.0.10))(react@18.3.1) @@ -2377,7 +2377,7 @@ importers: version: 3.592.0 '@aws-sdk/credential-providers': specifier: 3.592.0 - version: 3.592.0(@aws-sdk/client-sso-oidc@3.645.0(@aws-sdk/client-sts@3.592.0)) + version: 3.592.0(@aws-sdk/client-sso-oidc@3.645.0(@aws-sdk/client-sts@3.645.0)) '@chromatic-com/storybook': specifier: ^1.5.0 version: 1.8.0(react@18.3.1)