From d9fb5441bc81080412250ff76f3b46d7b144fd56 Mon Sep 17 00:00:00 2001 From: kumaryash90 Date: Wed, 11 Dec 2024 18:09:48 +0000 Subject: [PATCH] [Dashboard | SDK] Feature: Ref values for address params in publish (#5360) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PROT-932 PROT-864 ## Problem solved https://linear.app/thirdweb/project/[contract-tooling]-deployment-publish-flow-revamp-0194e04e2a84/overview Handle contract refs in publish form -- Resolve / deploy all linked contracts in publish metadata, and pass the addresses as constructor args for corresponding params. It recursively deploys all referenced contracts through the ref chain, before deploying the main contract. --- ## PR-Codex overview This PR focuses on enhancing the contract deployment and parameter handling in the thirdweb platform, introducing dynamic parameters, improved UI components, and better error handling for contract interactions. ### Detailed summary - Updated `deploy-with-abi.ts` to return the contract address instead of throwing an error if already deployed. - Enhanced UI components in `string-input.tsx` and `raw-input.tsx` for better user experience. - Added dynamic parameter handling in multiple components. - Introduced new types for dynamic parameters in `deploy-metadata.ts`. - Improved form handling in `FormFieldSetup` and related components. - Added reference contract input fields in various components for dynamic contract deployments. - Enhanced error handling and validation in the deployment forms. - Updated component structure for better maintainability and clarity. > The following files were skipped due to too many changes: `apps/dashboard/src/components/contract-components/contract-publish-form/contract-params-fieldset.tsx` > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` --- .../@/components/blocks/FormFieldSetup.tsx | 5 +- .../contracts/publish/[publish_uri]/page.tsx | 2 +- .../contract-deploy-form/custom-contract.tsx | 47 +- .../trusted-forwarders-fieldset.tsx | 106 ++-- .../contract-params-fieldset.tsx | 503 ++++++++++-------- .../decoded-input-array-fieldset.tsx | 88 +++ .../decoded-bytes-input/decoded-input-set.tsx | 91 ++++ .../decoded-bytes-input/decoded-input.tsx | 127 +++++ .../ref-bytes-input-fieldset.tsx | 63 +++ .../decoded-bytes-input/ref-bytes-input.tsx | 204 +++++++ .../impl-params-fieldset.tsx | 176 ++++++ .../contract-publish-form/index.tsx | 51 +- .../ref-input-impl-fieldset.tsx | 62 +++ .../ref-input-impl.tsx | 201 +++++++ .../ref-contract-input/ref-input-fieldset.tsx | 61 +++ .../ref-contract-input/ref-input.tsx | 201 +++++++ .../fetchPublishedContracts.ts | 9 +- .../components/solidity-inputs/raw-input.tsx | 11 +- .../solidity-inputs/string-input.tsx | 2 + .../contract/deployment/deploy-with-abi.ts | 2 +- .../extensions/prebuilts/deploy-published.ts | 42 +- .../prebuilts/process-ref-deployments.test.ts | 196 +++++++ .../prebuilts/process-ref-deployments.ts | 167 ++++++ .../src/extensions/thirdweb/write/publish.ts | 1 + .../src/utils/any-evm/deploy-metadata.ts | 29 + 25 files changed, 2136 insertions(+), 311 deletions(-) create mode 100644 apps/dashboard/src/components/contract-components/contract-publish-form/decoded-bytes-input/decoded-input-array-fieldset.tsx create mode 100644 apps/dashboard/src/components/contract-components/contract-publish-form/decoded-bytes-input/decoded-input-set.tsx create mode 100644 apps/dashboard/src/components/contract-components/contract-publish-form/decoded-bytes-input/decoded-input.tsx create mode 100644 apps/dashboard/src/components/contract-components/contract-publish-form/decoded-bytes-input/ref-bytes-input-fieldset.tsx create mode 100644 apps/dashboard/src/components/contract-components/contract-publish-form/decoded-bytes-input/ref-bytes-input.tsx create mode 100644 apps/dashboard/src/components/contract-components/contract-publish-form/impl-params-fieldset.tsx create mode 100644 apps/dashboard/src/components/contract-components/contract-publish-form/ref-contract-impl-input/ref-input-impl-fieldset.tsx create mode 100644 apps/dashboard/src/components/contract-components/contract-publish-form/ref-contract-impl-input/ref-input-impl.tsx create mode 100644 apps/dashboard/src/components/contract-components/contract-publish-form/ref-contract-input/ref-input-fieldset.tsx create mode 100644 apps/dashboard/src/components/contract-components/contract-publish-form/ref-contract-input/ref-input.tsx create mode 100644 packages/thirdweb/src/extensions/prebuilts/process-ref-deployments.test.ts create mode 100644 packages/thirdweb/src/extensions/prebuilts/process-ref-deployments.ts diff --git a/apps/dashboard/src/@/components/blocks/FormFieldSetup.tsx b/apps/dashboard/src/@/components/blocks/FormFieldSetup.tsx index 3483026c579..ce8b44387c1 100644 --- a/apps/dashboard/src/@/components/blocks/FormFieldSetup.tsx +++ b/apps/dashboard/src/@/components/blocks/FormFieldSetup.tsx @@ -3,16 +3,17 @@ import { ToolTipLabel } from "@/components/ui/tooltip"; import { AsteriskIcon, InfoIcon } from "lucide-react"; export function FormFieldSetup(props: { - htmlFor: string; + htmlFor?: string; label: string; errorMessage: React.ReactNode | undefined; children: React.ReactNode; tooltip?: React.ReactNode; isRequired: boolean; helperText?: React.ReactNode; + className?: string; }) { return ( -
+
diff --git a/apps/dashboard/src/app/(dashboard)/contracts/publish/[publish_uri]/page.tsx b/apps/dashboard/src/app/(dashboard)/contracts/publish/[publish_uri]/page.tsx index 0ac4bc08aa9..6921417ff96 100644 --- a/apps/dashboard/src/app/(dashboard)/contracts/publish/[publish_uri]/page.tsx +++ b/apps/dashboard/src/app/(dashboard)/contracts/publish/[publish_uri]/page.tsx @@ -73,7 +73,7 @@ export default async function PublishContractPage( } return ( -
+
; + deployParams: Record; moduleData: Record>; contractMetadata?: { name: string; @@ -80,6 +80,18 @@ type CustomContractDeploymentFormData = { recipients?: Recipient[]; }; +export interface DynamicValue { + dynamicValue: { + type: string; + refContracts?: { + publisherAddress: string; + version: string; + contractId: string; + salt?: string; + }[]; + }; +} + export type CustomContractDeploymentForm = UseFormReturn; @@ -159,6 +171,8 @@ export const CustomContractForm: React.FC = ({ "initialize", ); + const implementationConstructorParams = metadata?.implConstructorParams; + const isFactoryDeployment = metadata?.isDeployableViaFactory || metadata?.isDeployableViaProxy || @@ -206,9 +220,16 @@ export const CustomContractForm: React.FC = ({ acc[param.name] = activeAccount.address; } + // specify refs if present + const dynamicValue = + metadata?.constructorParams?.[param.name]?.dynamicValue; + if (dynamicValue && acc[param.name] === "") { + acc[param.name] = { dynamicValue }; + } + return acc; }, - {} as Record, + {} as Record, ), }), [deployParams, metadata?.constructorParams, activeAccount, walletChain?.id], @@ -353,7 +374,11 @@ export const CustomContractForm: React.FC = ({ const contructorParams = metadata?.constructorParams || {}; const extraMetadataParam = contructorParams[paramKey]; - if (shouldHide(paramKey) || !extraMetadataParam?.hidden) { + if ( + shouldHide(paramKey) || + extraMetadataParam?.hidden !== true || + extraMetadataParam?.dynamicValue + ) { return null; } @@ -412,11 +437,12 @@ export const CustomContractForm: React.FC = ({ params: { name: params.contractMetadata?.name || "", contractURI: _contractURI, - defaultAdmin: params.deployParams._defaultAdmin, + defaultAdmin: params.deployParams._defaultAdmin as string, platformFeeBps: Number(params.deployParams._platformFeeBps), - platformFeeRecipient: params.deployParams._platformFeeRecipient, + platformFeeRecipient: params.deployParams + ._platformFeeRecipient as string, trustedForwarders: params.deployParams._trustedForwarders - ? JSON.parse(params.deployParams._trustedForwarders) + ? JSON.parse(params.deployParams._trustedForwarders as string) : undefined, }, }); @@ -442,6 +468,7 @@ export const CustomContractForm: React.FC = ({ client: thirdwebClient, deployMetadata: metadata, initializeParams, + implementationConstructorParams, salt, modules: modules?.map((m) => ({ deployMetadata: m, @@ -652,7 +679,7 @@ export const CustomContractForm: React.FC = ({ ).error?.message, }} royaltyBps={{ - value: form.watch("deployParams._royaltyBps"), + value: form.watch("deployParams._royaltyBps") as string, isInvalid: !!form.getFieldState( "deployParams._royaltyBps", form.formState, @@ -749,7 +776,11 @@ export const CustomContractForm: React.FC = ({ const contructorParams = metadata?.constructorParams || {}; const extraMetadataParam = contructorParams[paramKey]; - if (shouldHide(paramKey) || extraMetadataParam?.hidden) { + if ( + shouldHide(paramKey) || + extraMetadataParam?.hidden === true || + extraMetadataParam?.dynamicValue + ) { return null; } diff --git a/apps/dashboard/src/components/contract-components/contract-deploy-form/trusted-forwarders-fieldset.tsx b/apps/dashboard/src/components/contract-components/contract-deploy-form/trusted-forwarders-fieldset.tsx index 3846b180e64..f848d722d4a 100644 --- a/apps/dashboard/src/components/contract-components/contract-deploy-form/trusted-forwarders-fieldset.tsx +++ b/apps/dashboard/src/components/contract-components/contract-deploy-form/trusted-forwarders-fieldset.tsx @@ -2,7 +2,10 @@ import { Flex, FormControl, InputGroup } from "@chakra-ui/react"; import { SolidityInput } from "contract-ui/components/solidity-inputs"; import { FormErrorMessage, FormHelperText, FormLabel } from "tw-components"; import { Fieldset } from "./common"; -import type { CustomContractDeploymentForm } from "./custom-contract"; +import type { + CustomContractDeploymentForm, + DynamicValue, +} from "./custom-contract"; interface TrustedForwardersFieldsetProps { form: CustomContractDeploymentForm; @@ -11,56 +14,65 @@ interface TrustedForwardersFieldsetProps { export const TrustedForwardersFieldset: React.FC< TrustedForwardersFieldsetProps > = ({ form }) => { + const isDynamicValue = (val: string | DynamicValue): val is DynamicValue => { + return typeof val === "object" && val !== null && "dynamicValue" in val; + }; + + const value = form.watch("deployParams._trustedForwarders"); return ( -
- -
- {/* left */} -
- Trusted Forwarders + <> + {!isDynamicValue(value) && ( +
+ +
+ {/* left */} +
+ Trusted Forwarders - - - Trusted forwarder addresses to enable ERC-2771 transactions - (i.e. gasless). - + + + Trusted forwarder addresses to enable ERC-2771 transactions + (i.e. gasless). + - - You can provide your own forwarder. - - -
-
+ + You can provide your own forwarder. + + +
+
-
- - - - - +
+ + + + + - - { - form.getFieldState( - "deployParams._trustedForwarders", - form.formState, - ).error?.message - } - -
- -
+ + { + form.getFieldState( + "deployParams._trustedForwarders", + form.formState, + ).error?.message + } + +
+ + + )} + ); }; diff --git a/apps/dashboard/src/components/contract-components/contract-publish-form/contract-params-fieldset.tsx b/apps/dashboard/src/components/contract-components/contract-publish-form/contract-params-fieldset.tsx index b9cdbef3c58..901be57a02b 100644 --- a/apps/dashboard/src/components/contract-components/contract-publish-form/contract-params-fieldset.tsx +++ b/apps/dashboard/src/components/contract-components/contract-publish-form/contract-params-fieldset.tsx @@ -1,29 +1,19 @@ -import { - Divider, - Flex, - FormControl, - Input, - InputGroup, - InputRightElement, - Textarea, - Tooltip, - useBreakpointValue, -} from "@chakra-ui/react"; +import { FormFieldSetup } from "@/components/blocks/FormFieldSetup"; +import { Checkbox, CheckboxWithLabel } from "@/components/ui/checkbox"; +import { InlineCode } from "@/components/ui/inline-code"; +import { Input } from "@/components/ui/input"; +import { Separator } from "@/components/ui/separator"; +import { Switch } from "@/components/ui/switch"; +import { Textarea } from "@/components/ui/textarea"; +import { FormControl, useBreakpointValue } from "@chakra-ui/react"; import type { AbiParameter } from "abitype"; import { SolidityInput } from "contract-ui/components/solidity-inputs"; import { camelToTitle } from "contract-ui/components/solidity-inputs/helpers"; import { getTemplateValuesForType } from "lib/deployment/template-values"; +import { useState } from "react"; import { useFormContext } from "react-hook-form"; -import { - Button, - Card, - Checkbox, - FormErrorMessage, - FormHelperText, - FormLabel, - Heading, - Text, -} from "tw-components"; +import { DecodedInputArrayFieldset } from "./decoded-bytes-input/decoded-input-array-fieldset"; +import { RefInputFieldset } from "./ref-contract-input/ref-input-fieldset"; interface ContractParamsFieldsetProps { deployParams: readonly AbiParameter[]; @@ -32,243 +22,288 @@ export const ContractParamsFieldset: React.FC = ({ deployParams, }) => { const form = useFormContext(); - const isMobile = useBreakpointValue({ base: true, md: false }); + const [isCustomInputEnabledArray, setIsCustomInputEnabledArray] = useState( + Array(deployParams.length).fill(false), + ); + + const handleCustomInputEnabledArrayChange = ( + index: number, + newValue: boolean, + ) => { + const newIsCustomInputEnabledArray = [...isCustomInputEnabledArray]; + newIsCustomInputEnabledArray[index] = newValue; + setIsCustomInputEnabledArray(newIsCustomInputEnabledArray); + + if (newValue) { + form.setValue( + `constructorParams.${deployParams[index]?.name || "*"}.defaultValue`, + "", + { + shouldDirty: true, + }, + ); + + form.setValue( + `constructorParams.${deployParams[index]?.name || "*"}.dynamicValue.type`, + deployParams[index]?.type, + ); + } else { + form.setValue( + `constructorParams.${deployParams[index]?.name || "*"}.dynamicValue.type`, + "", + ); + form.setValue( + `constructorParams.${deployParams[index]?.name || "*"}.dynamicValue`, + "", + { + shouldDirty: true, + }, + ); + } + }; + return ( - - - Contract Parameters - - These are the parameters users will need to fill in when deploying - this contract. - - - +
+

+ Contract Parameters +

+

+ These are the parameters users will need to fill in when deploying this + contract. +

+ +
+ +
{deployParams.map((param, idx) => { const paramTemplateValues = getTemplateValuesForType(param.type); return ( - - - {param.name ? ( - {param.name} - ) : ( - - Unnamed param (will not be used) - - )} - {param.type} - - - - - - Display Name - - + {/* Title + Type */} +
+

+ {param.name ? ( + param.name + ) : ( + + Unnamed param (will not be used) + + )} +

+ +
+ +
+ + {/* Display Name */} + + + form.setValue( + `constructorParams.${ + param.name ? param.name : "*" + }.displayName`, + e.target.value, + ) + } + placeholder={camelToTitle(param.name ? param.name : "*")} + /> + + +
+ + {/* Description */} + + {form.watch( + `constructorParams.${ + param.name ? param.name : "*" + }.description`, + )?.length ?? 0} + /400 characters + + } + > +