Modular contracts Edit Extensions UI (#2225)

<!-- start pr-codex -->

## PR-Codex overview
This PR adds support for Modular Core and Modular Extension features, updates contract extension settings UI, and enhances extension management in the contract editor.

### Detailed summary
- Added support for `ModularCore` and `ModularExtension` features
- Updated contract extension settings UI
- Improved extension management in the contract editor

> The following files were skipped due to too many changes: `src/contract-ui/tabs/edit-extensions/components/InstalledExtensionsTable.tsx`, `src/contract-ui/tabs/edit-extensions/components/ExtensionForm.tsx`, `pnpm-lock.yaml`

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

<!-- end pr-codex -->
This commit is contained in:
kumaryash90
2024-06-14 21:37:25 +00:00
parent 4112dcf4cc
commit 2f439ba4b3
13 changed files with 2313 additions and 1612 deletions
+7 -7
View File
@@ -46,13 +46,13 @@
"@tanstack/react-query": "^4.36.1",
"@tanstack/react-query-persist-client": "^4.36.1",
"@tanstack/react-table": "^8.17.3",
"@thirdweb-dev/chains": "0.1.117",
"@thirdweb-dev/react": "4.8.0",
"@thirdweb-dev/react-core": "4.8.0",
"@thirdweb-dev/sdk": "4.0.94",
"@thirdweb-dev/service-utils": "0.4.31",
"@thirdweb-dev/chains": "0.1.118",
"@thirdweb-dev/react": "4.9.0",
"@thirdweb-dev/react-core": "4.9.0",
"@thirdweb-dev/sdk": "4.0.95",
"@thirdweb-dev/service-utils": "0.4.32",
"@thirdweb-dev/storage": "2.0.15",
"@thirdweb-dev/wallets": "2.5.33",
"@thirdweb-dev/wallets": "2.5.35",
"@vercel/og": "^0.6.2",
"airtable": "^0.12.2",
"chakra-react-select": "^4.7.6",
@@ -101,7 +101,7 @@
"swagger-ui-react": "^5.17.14",
"tailwind-merge": "^2.3.0",
"tailwindcss-animate": "^1.0.7",
"thirdweb": "5.29.0",
"thirdweb": "5.29.2",
"tiny-invariant": "^1.3.3",
"use-debounce": "^10.0.1",
"zksync-ethers": "^5.8.0",
+1281 -1597
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -126,7 +126,7 @@ export const TransactionButton: React.FC<TransactionButtonProps> = ({
size === "sm" ? 3 : size === "lg" ? 6 : size === "xs" ? 2 : 4
}))`
}
isDisabled={isChainDeprecated}
isDisabled={isChainDeprecated || restButtonProps.isDisabled}
>
{children}
<Tooltip
@@ -15,8 +15,8 @@ export const DynamicContractsFieldset = () => {
return (
<Flex gap={8} direction="column" as="fieldset">
<Flex gap={2} direction="column">
<Heading size="title.md">Dynamic Contract settings</Heading>
<Text>You can set default extensions for your dynamic contract.</Text>
<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) => (
@@ -333,6 +333,9 @@ export const ContractPublishForm: React.FC<ContractPublishFormProps> = ({
data.factoryDeploymentData?.customFactoryInput
?.customFactoryAddresses,
},
modularFactoryInput: {
hooksParamName: "",
},
},
},
contractName: publishMetadata.data?.name,
+48 -4
View File
@@ -32,7 +32,10 @@ import {
useSigner,
useWalletConfig,
} from "@thirdweb-dev/react";
import { FeatureWithEnabled } from "@thirdweb-dev/sdk/dist/declarations/src/evm/constants/contract-features";
import {
FeatureName,
FeatureWithEnabled,
} from "@thirdweb-dev/sdk/dist/declarations/src/evm/constants/contract-features";
import { DeploymentTransaction } from "@thirdweb-dev/sdk/dist/declarations/src/evm/types/any-evm/deploy-data";
import {
Abi,
@@ -46,9 +49,11 @@ import {
extractEventsFromAbi,
extractFunctionParamsFromAbi,
extractFunctionsFromAbi,
fetchContractMetadata,
fetchPreDeployMetadata,
fetchRawPredeployMetadata,
getTrustedForwarders,
isExtensionEnabled,
} from "@thirdweb-dev/sdk";
import {
getZkTransactionsForDeploy,
@@ -889,18 +894,57 @@ export async function fetchPublishedContracts(
);
}
export 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) => {
const extensions = detectFeatures(c.deployMetadata.abi as Abi);
return isExtensionEnabled(c.deployMetadata.abi as Abi, feature, extensions);
});
}
export type PublishedContractDetails = Awaited<
ReturnType<typeof fetchPublishedContracts>
>[number];
export function usePublishedContractsQuery(address?: string) {
export function usePublishedContractsQuery(
address?: string,
feature?: FeatureName,
) {
const sdk = getThirdwebSDK(Polygon.chainId, getDashboardChainRpc(Polygon));
const queryClient = useQueryClient();
return useQuery<PublishedContractDetails[]>(
["published-contracts", address],
["published-contracts", address, feature],
() => {
invariant(sdk, "sdk not provided");
return fetchPublishedContracts(sdk, queryClient, address);
return feature && feature.length > 0
? fetchPublishedContractsWithFeature(sdk, queryClient, feature, address)
: fetchPublishedContracts(sdk, queryClient, address);
},
{
enabled: !!address && !!sdk,
+14
View File
@@ -97,6 +97,11 @@ const LazyContractSourcesPage = dynamic(() =>
({ ContractSourcesPage }) => ContractSourcesPage,
),
);
const LazyContractEditExtensionsPage = dynamic(() =>
import("../tabs/edit-extensions/page").then(
({ ContractEditExtensionsPage }) => ContractEditExtensionsPage,
),
);
export function useContractRouteConfig(
contractAddress: string,
@@ -129,6 +134,15 @@ export function useContractRouteConfig(
component: ContractOverviewPage,
isDefault: true,
},
{
title: "Edit extensions",
path: "edit-extensions",
isEnabled: extensionDetectedState({
contractQuery,
feature: ["ModularCore"],
}),
component: LazyContractEditExtensionsPage,
},
{
title: "Code Snippets",
path: "code",
@@ -0,0 +1,408 @@
import { useForm } from "react-hook-form";
import {
Flex,
Input,
Select,
FormControl,
Skeleton,
Spacer,
Grid,
useToast,
Spinner,
} from "@chakra-ui/react";
import { FormLabel, FormErrorMessage, Text } from "tw-components";
import {
useAllVersions,
usePublishedContractsQuery,
} from "components/contract-components/hooks";
import { installPublishedExtension } from "thirdweb/extensions/modular";
import {
sendTransaction,
waitForReceipt,
type ContractOptions,
} from "thirdweb";
import type { Account } from "thirdweb/wallets";
import { useMutation, useQuery } from "@tanstack/react-query";
import { thirdwebClient } from "../../../../@/constants/client";
import { compatibleExtensions } from "@thirdweb-dev/sdk";
import { download } from "thirdweb/storage";
import { resolveImplementation } from "thirdweb/utils";
import { TransactionButton } from "components/buttons/TransactionButton";
type FormData = {
publisherAddress: string;
extensionContract: string;
version: string;
};
type InstallExtensionFormProps = {
contract: ContractOptions;
refetchExtensions: () => void;
account: Account;
installedExtensions: {
data?: string[];
isLoading: boolean;
};
};
export const InstallExtensionForm = (props: InstallExtensionFormProps) => {
const { register, watch, formState, resetField, reset } = useForm<FormData>({
defaultValues: {
version: "latest",
},
});
const toast = useToast();
const { contract, account } = props;
const { errors } = formState;
const publishedContractsQuery = usePublishedContractsQuery(
watch("publisherAddress"),
"ModularExtension",
);
const allVersions = useAllVersions(
watch("publisherAddress"),
watch("extensionContract"),
);
const installMutation = useMutation({
mutationFn: async () => {
const installTransaction = installPublishedExtension({
contract,
chain: contract.chain,
client: contract.client,
account,
extensionName: watch("extensionContract"),
publisherAddress: watch("publisherAddress"),
version: watch("version"),
});
const txResult = await sendTransaction({
transaction: installTransaction,
account,
});
await waitForReceipt(txResult);
},
onSuccess() {
props.refetchExtensions();
toast({
variant: "solid",
position: "bottom",
title: "Extension installed successfully",
status: "success",
duration: 5000,
isClosable: true,
});
// clear form
reset({
publisherAddress: "",
extensionContract: "",
version: "latest",
});
},
onError(err) {
toast({
variant: "solid",
position: "bottom",
title: "Failed to install extension",
status: "error",
duration: 5000,
isClosable: true,
});
console.error("Error during installation:", err);
},
});
const onSubmit = async () => {
installMutation.mutate();
};
const extensionContractInputProps = register("extensionContract", {
required: "Extension name is required",
});
const selectedExtension = publishedContractsQuery.data?.find(
(x) => x.id === watch("extensionContract"),
);
// Get core contract bytecode
const coreContractByteCodeQuery = useQuery({
queryKey: ["getBytecode", contract.address, contract.chain.id],
queryFn: async () => {
const coreImplementation = await resolveImplementation(contract);
return coreImplementation.bytecode;
},
retry: false,
// 30 minutes
// staleTime: 1000 * 60 * 30,
});
// Get Installed extension bytecodes
const installedExtensionBytecodesQuery = useQuery({
queryKey: [
"installedExtensionBytecodes",
contract.address,
contract.chain.id,
props.installedExtensions.data,
],
queryFn: async () => {
const extensionAddress = props.installedExtensions.data;
if (!extensionAddress) {
return [];
}
return Promise.all(
extensionAddress.map(async (address) => {
const result = await resolveImplementation({
client: thirdwebClient,
address,
chain: contract.chain,
});
if (!result) {
throw new Error("Failed to fetch bytecode for extension");
}
return result.bytecode;
}),
);
},
enabled: !!props.installedExtensions.data,
retry: false,
// 30 minutes
staleTime: 1000 * 60 * 30,
});
// check if selected extension is compatible with the core contract and installed extensions
const isExtensionCompatibleQuery = useQuery({
queryKey: [
"isExtensionCompatible",
contract.address,
contract.chain.id,
installedExtensionBytecodesQuery.data,
coreContractByteCodeQuery.data,
selectedExtension,
],
queryFn: async () => {
if (
!coreContractByteCodeQuery.data ||
!installedExtensionBytecodesQuery.data ||
!selectedExtension
) {
throw new Error("Unexpected error");
}
return isExtensionCompatible({
contractInfo: {
bytecode: coreContractByteCodeQuery.data,
installedExtensionBytecodes: installedExtensionBytecodesQuery.data,
chainId: contract.chain.id,
},
extensionInfo: {
bytecodeUri: selectedExtension.metadata.bytecodeUri,
},
});
},
retry: false,
// 30 minutes
staleTime: 1000 * 60 * 30,
enabled:
!!selectedExtension &&
!!coreContractByteCodeQuery.data &&
!!installedExtensionBytecodesQuery.data,
});
return (
<form
onSubmit={(e) => {
e.preventDefault();
onSubmit();
}}
>
<Grid
gap={4}
gridTemplateColumns={{
base: "1fr",
md: "1fr 1fr",
lg: "1fr 1fr 1fr",
}}
>
<FormControl isInvalid={!!errors.publisherAddress}>
<FormLabel>Publisher</FormLabel>
<Input
disabled={installMutation.isLoading}
bg="backgroundHighlight"
placeholder="Publisher address"
{...register("publisherAddress", {
required: "Publisher address is required",
})}
/>
<FormErrorMessage>
{errors.publisherAddress && errors.publisherAddress.message}
</FormErrorMessage>
</FormControl>
<FormControl
isInvalid={
!!errors.extensionContract ||
isExtensionCompatibleQuery.data === false
}
isRequired={true}
>
<FormLabel>Extension Name</FormLabel>
<Skeleton
isLoaded={
!!publishedContractsQuery.data ||
!publishedContractsQuery.isFetching
}
borderRadius="lg"
>
<Select
disabled={
installMutation.isLoading ||
publishedContractsQuery?.data?.length === 0 ||
publishedContractsQuery.isLoading
}
bg="backgroundHighlight"
{...extensionContractInputProps}
onChange={(e) => {
// reset version when extension changes
resetField("version");
extensionContractInputProps.onChange(e);
}}
placeholder={
publishedContractsQuery.data?.length === 0
? "No extensions"
: "Select extension"
}
>
{publishedContractsQuery?.data?.map(({ id }) => (
<option key={id} value={id}>
{id}
</option>
))}
</Select>
</Skeleton>
<FormErrorMessage fontWeight={500}>
{!isExtensionCompatibleQuery.isFetching &&
isExtensionCompatibleQuery.data === false &&
"Extension is not compatible"}
{errors.extensionContract && errors.extensionContract.message}
</FormErrorMessage>
{isExtensionCompatibleQuery.isFetching && selectedExtension && (
<Flex alignItems="center" gap={1.5} mt={2}>
<Text size="body.sm" fontWeight={500} color="blue.500">
Checking Compatibility
</Text>
<Spinner size={"xs"} color="blue.500" />
</Flex>
)}
{isExtensionCompatibleQuery.isError && (
<Flex alignItems="center" gap={2} mt={2}>
<Text
fontWeight={500}
size="body.sm"
_dark={{
color: "yellow.500",
}}
_light={{
color: "yellow.700",
}}
>
Extension may not be compatible
</Text>
</Flex>
)}
</FormControl>
<FormControl isInvalid={!!errors.version} isRequired={true}>
<FormLabel>Extension Version</FormLabel>
<Skeleton isLoaded={!allVersions.isFetching} borderRadius="lg">
<Select
disabled={
!allVersions.data ||
allVersions.isLoading ||
isExtensionCompatibleQuery.data === false ||
installMutation.isLoading ||
isExtensionCompatibleQuery.isFetching
}
bg="backgroundHighlight"
w="full"
{...register("version", {
required: "Version is required",
})}
>
<option value="latest">Latest</option>
{allVersions?.data?.map(({ version }) => (
<option key={version} value={version}>
{version}
</option>
))}
</Select>
</Skeleton>
<FormErrorMessage>
{errors.version && errors.version.message}
</FormErrorMessage>
</FormControl>
</Grid>
<Spacer h={5} />
{/* Submit */}
<Flex justify={"flex-end"}>
<TransactionButton
transactionCount={1}
isLoading={installMutation.isLoading}
type="submit"
colorScheme="primary"
alignSelf="flex-end"
isDisabled={
!formState.isValid ||
isExtensionCompatibleQuery.data === false ||
isExtensionCompatibleQuery.isFetching
}
>
Install
</TransactionButton>
</Flex>
</form>
);
};
async function isExtensionCompatible(options: {
contractInfo: {
bytecode: string;
installedExtensionBytecodes: string[];
chainId: number;
};
extensionInfo: {
bytecodeUri: string;
};
}) {
// 1. get extension's bytecode
const res = await download({
client: thirdwebClient,
uri: options.extensionInfo.bytecodeUri,
});
const extensionBytecode = await res.text();
// 2. check compatibility with core and installed extensions
try {
const isCompatible = await compatibleExtensions(
options.contractInfo.bytecode,
[...options.contractInfo.installedExtensionBytecodes, extensionBytecode],
options.contractInfo.chainId,
);
return isCompatible;
} catch (e) {
console.error("Error during compatibility check:", e);
throw e;
}
}
@@ -0,0 +1,346 @@
import {
Flex,
Table,
TableContainer,
Tbody,
Td,
Th,
Thead,
Tr,
Tooltip,
Skeleton,
Alert,
AlertIcon,
useToast,
Box,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
useDisclosure,
} from "@chakra-ui/react";
import { FaRegTrashAlt } from "react-icons/fa";
import { Button, Text, Heading } from "tw-components";
import { uninstallExtensionByProxy } from "thirdweb/extensions/modular";
import {
sendTransaction,
waitForReceipt,
type ContractOptions,
} from "thirdweb";
import { AddressCopyButton } from "tw-components/AddressCopyButton";
import { useExtensionContractInfo } from "./extensionContractInfo";
import type { Account } from "thirdweb/wallets";
import { TransactionButton } from "components/buttons/TransactionButton";
import { useMutation } from "@tanstack/react-query";
export const InstalledExtensionsTable = (props: {
contract: ContractOptions;
installedExtensions: {
data?: string[];
isLoading: boolean;
};
refetchExtensions: () => void;
ownerAccount?: Account;
}) => {
const { installedExtensions, ownerAccount } = props;
if (
!installedExtensions.isLoading &&
installedExtensions.data?.length === 0
) {
return (
<>
<Heading as="h2" size="title.md">
Installed Extensions
</Heading>
<Alert
status="error"
fontSize={"small"}
borderRadius="md"
p={3}
bg="alertBg"
>
<AlertIcon />
<Flex direction="column" gap={0.5} ml={1}>
<Text color="heading" size="body.lg" fontWeight={500}>
No extensions installed
</Text>
</Flex>
</Alert>
</>
);
}
return (
<>
<Heading as="h2" size="title.md">
Installed Extensions
</Heading>
<TableContainer
rounded={"lg"}
overflowX={{ base: "auto", md: "initial" }}
>
<Table position="relative">
<Thead>
<TableHeading> Extension Name </TableHeading>
<TableHeading> Description </TableHeading>
<TableHeading> Publisher Address </TableHeading>
<TableHeading> Extension Address </TableHeading>
<TableHeading> Version </TableHeading>
{ownerAccount && <TableHeading> Remove </TableHeading>}
</Thead>
<Tbody>
{installedExtensions.isLoading ? (
<>
<SkeletonRow ownerAccount={ownerAccount} />
<SkeletonRow ownerAccount={ownerAccount} />
<SkeletonRow ownerAccount={ownerAccount} />
</>
) : (
<>
{installedExtensions.data?.map((e, i) => (
<ExtensionRow
key={i}
extensionAddress={e}
contract={props.contract}
onRemoveExtension={props.refetchExtensions}
ownerAccount={ownerAccount}
/>
))}
</>
)}
</Tbody>
</Table>
</TableContainer>
</>
);
};
function SkeletonRow(props: { ownerAccount?: Account }) {
return (
<Tr borderBottomWidth={1} _last={{ borderBottomWidth: 0 }}>
<TableData>
<Skeleton height="24px" rounded="xl" />
</TableData>
<TableData>
<Skeleton height="24px" rounded="xl" />
</TableData>
<TableData>
<Skeleton height="24px" rounded="xl" />
</TableData>
<TableData>
<Skeleton height="24px" rounded="xl" />
</TableData>
{/* Version */}
<TableData>
<Skeleton height="24px" rounded="xl" />
</TableData>
{/* Remove */}
{props.ownerAccount && (
<TableData>
<Skeleton height="24px" rounded="xl" />
</TableData>
)}
</Tr>
);
}
function ExtensionRow(props: {
extensionAddress: string;
contract: ContractOptions;
onRemoveExtension: () => void;
ownerAccount?: Account;
}) {
const { contract, extensionAddress, ownerAccount } = props;
const toast = useToast();
const { isOpen, onOpen, onClose } = useDisclosure();
const contractInfo = useExtensionContractInfo(props.extensionAddress);
const uninstallMutation = useMutation({
mutationFn: async (account: Account) => {
const uninstallTransaction = uninstallExtensionByProxy({
contract,
chain: contract.chain,
client: contract.client,
extensionProxyAddress: extensionAddress,
extensionData: "0x",
});
const txResult = await sendTransaction({
transaction: uninstallTransaction,
account,
});
await waitForReceipt(txResult);
},
onSuccess() {
toast({
variant: "solid",
position: "bottom",
title: "Extension Removed successfully",
status: "success",
duration: 5000,
isClosable: true,
});
props.onRemoveExtension();
},
onError(error) {
toast({
variant: "solid",
position: "bottom",
title: "Failed to remove extension",
status: "error",
duration: 5000,
isClosable: true,
});
console.error("Error during uninstallation:", error);
},
});
const handleRemove = async () => {
if (!ownerAccount) {
return;
}
onClose();
uninstallMutation.mutate(ownerAccount);
};
if (!contractInfo) {
return <SkeletonRow ownerAccount={ownerAccount} />;
}
return (
<Tr borderBottomWidth={1} _last={{ borderBottomWidth: 0 }}>
<TableData>
<Text color="heading">{contractInfo.name}</Text>
</TableData>
<TableData>
<Text>{contractInfo.description || "..."}</Text>
</TableData>
<TableData>
<AddressCopyButton size="xs" address={contractInfo.publisher} />
</TableData>
<TableData>
<AddressCopyButton size="xs" address={extensionAddress} />
</TableData>
{/* Version */}
<TableData>
<Text>{contractInfo.version}</Text>
</TableData>
{/* Remove */}
{ownerAccount && (
<TableData>
<Flex>
<Tooltip
label="Remove"
bg="backgroundHighlight"
border="1px solid"
borderColor="borderColor"
color="accent.500"
rounded="xl"
placement="top"
fontSize="small"
px={3}
py={2}
>
<Button
isLoading={uninstallMutation.isLoading}
onClick={onOpen}
color="red.500"
variant="outline"
_hover={{
bg: "accent.100",
}}
rounded={"xl"}
p={2}
>
<FaRegTrashAlt
style={{
width: "16px",
height: "16px",
}}
/>
</Button>
</Tooltip>
</Flex>
</TableData>
)}
<Modal isOpen={isOpen} onClose={onClose} isCentered>
<ModalOverlay />
<ModalContent>
<form
onSubmit={(e) => {
e.preventDefault();
handleRemove();
}}
>
<ModalHeader fontSize={"larger"} pb={1}>
Uninstall Extension
</ModalHeader>
<ModalCloseButton />
<ModalBody>
Are you sure you want to uninstall{" "}
<Box as="span" fontWeight={500} color="heading">
{contractInfo.name}
</Box>{" "}
?
</ModalBody>
<ModalFooter pt={10} gap={3}>
<Button
type="button"
mr={3}
m={0}
onClick={onClose}
variant="outline"
>
Cancel
</Button>
<TransactionButton
transactionCount={1}
isLoading={uninstallMutation.isLoading}
type="submit"
colorScheme="red"
alignSelf="flex-end"
mr={3}
m={0}
>
Uninstall
</TransactionButton>
</ModalFooter>
</form>
</ModalContent>
</Modal>
</Tr>
);
}
function TableData(props: { children: React.ReactNode }) {
return (
<Td borderBottomWidth="inherit" borderBottomColor="borderColor" py={5}>
{props.children}
</Td>
);
}
function TableHeading(props: { children: React.ReactNode }) {
return (
<Th border="none" color="faded">
{props.children}
</Th>
);
}
@@ -0,0 +1,37 @@
import { useEVMContractInfo } from "@3rdweb-sdk/react";
import { useAddress } from "@thirdweb-dev/react";
import { usePublishedContractsFromDeploy } from "components/contract-components/hooks";
import { THIRDWEB_DEPLOYER_ADDRESS } from "constants/addresses";
import { useMemo } from "react";
export function useExtensionContractInfo(contractAddress: string) {
const address = useAddress();
const activeNetworkInfo = useEVMContractInfo();
const publishedContractsFromDeploy = usePublishedContractsFromDeploy(
contractAddress,
activeNetworkInfo?.chain?.chainId,
);
const publishedContractToShow = useMemo(() => {
const reversedPublishedContractsFromDeploy = [
...(publishedContractsFromDeploy.data || []),
].reverse();
return (
reversedPublishedContractsFromDeploy.find(
(publishedContract) => publishedContract.publisher === address,
) ||
reversedPublishedContractsFromDeploy.find(
(publishedContract) =>
publishedContract.publisher === THIRDWEB_DEPLOYER_ADDRESS,
) ||
reversedPublishedContractsFromDeploy[
reversedPublishedContractsFromDeploy.length - 1
] ||
undefined
);
}, [publishedContractsFromDeploy.data, address]);
return publishedContractToShow as typeof publishedContractToShow | undefined;
}
@@ -0,0 +1,157 @@
import {
Stack,
Alert,
AlertIcon,
Flex,
Spacer,
Spinner,
} from "@chakra-ui/react";
import { InstalledExtensionsTable } from "./components/InstalledExtensionsTable";
import { InstallExtensionForm } from "./components/ExtensionForm";
import { Heading, Text } from "../../../tw-components";
import { useEVMContractInfo } from "../../../@3rdweb-sdk/react";
import { defineChain, getContract } from "thirdweb";
import { getInstalledExtensions, owner } from "thirdweb/extensions/modular";
import { useActiveAccount, useReadContract } from "thirdweb/react";
import { thirdwebClient } from "../../../lib/thirdweb-client";
import { useMemo } from "react";
interface ContractEditExtensionsPageProps {
contractAddress?: string;
}
export const ContractEditExtensionsPage: React.FC<
ContractEditExtensionsPageProps
> = ({ contractAddress }) => {
const contractInfo = useEVMContractInfo();
const chainId = contractInfo?.chain?.chainId;
if (!contractAddress || !chainId) {
return (
<Flex flex={1} alignItems="center" justifyContent="center" h={[300, 500]}>
<Spinner size="xl" />
</Flex>
);
}
return <Content contractAddress={contractAddress} chainId={chainId} />;
};
function Content(props: { contractAddress: string; chainId: number }) {
const { contractAddress, chainId } = props;
const account = useActiveAccount();
const contract = useMemo(
() =>
getContract({
client: thirdwebClient,
address: contractAddress,
chain: defineChain(chainId),
}),
[contractAddress, chainId],
);
const installedExtensionsQuery = useReadContract(getInstalledExtensions, {
contract,
});
const ownerQuery = useReadContract(owner, {
contract,
});
function refetchExtensions() {
installedExtensionsQuery.refetch();
}
if (ownerQuery.isLoading) {
return (
<Flex flex={1} alignItems="center" justifyContent="center" h={[300, 500]}>
<Spinner size="xl" />
</Flex>
);
}
if (!ownerQuery.data) {
return (
<Flex flex={1} alignItems="center" justifyContent="center" h={[300, 500]}>
<Text> Failed to resolve contract owner </Text>
</Flex>
);
}
const isOwner = ownerQuery.data === account?.address;
const installedExtensions = {
isLoading: installedExtensionsQuery.isLoading,
data: installedExtensionsQuery.data
? installedExtensionsQuery.data.map((x) => x.implementation)
: [],
};
return (
<Stack direction="column" spacing={4}>
{/* Alert */}
{isOwner && (
<>
<Heading as="h2" size="title.md">
Edit Extensions
</Heading>
<Alert
status="info"
fontSize={"small"}
borderRadius="md"
p={3}
bg="alertBg"
borderLeft="4px solid"
borderLeftColor="blue.500"
>
<AlertIcon />
<Flex direction="column" gap={0.5} ml={1}>
<Text color="heading" size="body.lg" fontWeight={500}>
Add capabilities to your contract by installing extensions.
</Text>
<Text>Lookup extensions to install in your contract</Text>
</Flex>
</Alert>
<Spacer h={10} />
<InstallExtensionForm
contract={contract}
refetchExtensions={refetchExtensions}
account={account}
installedExtensions={installedExtensions}
/>
</>
)}
{!isOwner && (
<>
<Alert
status="error"
fontSize={"small"}
borderRadius="md"
p={3}
bg="alertBg"
>
<AlertIcon />
<Flex direction="column" gap={0.5} ml={1}>
<Text color="heading" size="body.lg" fontWeight={500}>
You do not have permissions to edit extensions
</Text>
<Text>Connect owner wallet to edit extensions</Text>
</Flex>
</Alert>
</>
)}
<Spacer h={14} />
<InstalledExtensionsTable
installedExtensions={installedExtensions}
refetchExtensions={refetchExtensions}
contract={contract}
ownerAccount={isOwner ? account : undefined}
/>
</Stack>
);
}
@@ -267,6 +267,8 @@ const GUIDES_FOR_EXTENSION: Record<FeatureName, Item[]> = {
PackVRF: [],
PluginRouter: [],
DynamicContract: [],
ModularCore: [],
ModularExtension: [],
DirectListings: [],
EnglishAuctions: [],
Offers: [],
@@ -427,6 +429,8 @@ const TEMPLATES_FOR_EXTENSION: Record<FeatureName, Item[]> = {
PackVRF: [],
PluginRouter: [],
DynamicContract: [],
ModularCore: [],
ModularExtension: [],
DirectListings: [],
EnglishAuctions: [],
Offers: [],
+5 -1
View File
@@ -73,7 +73,7 @@ const chakraTheme: Theme = extendTheme({
border: "3px solid",
borderColor: "transparent",
borderRadius: "md",
background: `linear-gradient(${bgColor}, ${bgColor}) padding-box,
background: `linear-gradient(${bgColor}, ${bgColor}) padding-box,
linear-gradient(135deg, ${lgFrom}, ${lgTo}, ${lgFrom}) border-box`,
transitionProperty: "opacity, background",
transitionDuration: "slow",
@@ -383,6 +383,10 @@ const chakraTheme: Theme = extendTheme({
bgWhite: { default: "#fff", _dark: "black" },
backgroundBody: { default: "backgroundLight", _dark: "backgroundDark" },
backgroundHighlight: { default: "white", _dark: "#131417" },
alertBg: {
default: "hsl(220 18% 92% / 1)",
_dark: "hsl(225 10% 12.5% / 1)",
},
backgroundCardHighlight: { default: "white", _dark: "#131417" },
wordmark: { default: "#262A36", _dark: "whiteAlpha.900" },
heading: { default: "#262A36", _dark: "#ECECEC" },