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