From d2597004b7ce648e01b83f18f357eaf0ba46a15d Mon Sep 17 00:00:00 2001 From: Adam Gall Date: Thu, 4 Apr 2024 09:59:41 -0400 Subject: [PATCH 01/55] Remove a requestWithRetries --- src/hooks/DAO/loaders/useFractalNode.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/hooks/DAO/loaders/useFractalNode.ts b/src/hooks/DAO/loaders/useFractalNode.ts index 293c9b2f2f..e9c62bd47f 100644 --- a/src/hooks/DAO/loaders/useFractalNode.ts +++ b/src/hooks/DAO/loaders/useFractalNode.ts @@ -8,7 +8,6 @@ import { NodeAction } from '../../../providers/App/node/action'; import { useNetworkConfig } from '../../../providers/NetworkConfig/NetworkConfigProvider'; import { Node } from '../../../types'; import { mapChildNodes } from '../../../utils/hierarchy'; -import { useAsyncRetry } from '../../utils/useAsyncRetry'; import { useLazyDAOName } from '../useDAOName'; import { useFractalModules } from './useFractalModules'; @@ -33,7 +32,6 @@ export const useFractalNode = ( const { getDaoName } = useLazyDAOName(); const lookupModules = useFractalModules(); - const { requestWithRetries } = useAsyncRetry(); const formatDAOQuery = useCallback((result: { data?: DAOQueryQuery }, _daoAddress: string) => { if (!result.data) { @@ -103,10 +101,7 @@ export const useFractalNode = ( try { if (!safeAPI) throw new Error('SafeAPI not set'); - safeInfo = await requestWithRetries( - () => safeAPI.getSafeInfo(utils.getAddress(_daoAddress)), - 5, - ); + safeInfo = await safeAPI.getSafeInfo(utils.getAddress(_daoAddress)); } catch (e) { reset({ error: true }); return; @@ -129,7 +124,7 @@ export const useFractalNode = ( payload: safeInfo, }); }, - [action, lookupModules, requestWithRetries, reset, safeAPI], + [action, lookupModules, reset, safeAPI], ); useEffect(() => { From 932fc1b27b23bfa446985952dae84530bbfbeae4 Mon Sep 17 00:00:00 2001 From: Adam Gall Date: Thu, 4 Apr 2024 10:02:08 -0400 Subject: [PATCH 02/55] Add new property to SafeInfoResponseWithGuard called `nonceWithPending`, which will be the next nonce to use which takes into account all "pending" transactions on the safe --- src/hooks/DAO/loaders/useFractalNode.ts | 16 ++++++++++------ src/hooks/DAO/loaders/useLoadDAONode.ts | 13 +++++++++---- src/types/safeGlobal.ts | 1 + 3 files changed, 20 insertions(+), 10 deletions(-) diff --git a/src/hooks/DAO/loaders/useFractalNode.ts b/src/hooks/DAO/loaders/useFractalNode.ts index e9c62bd47f..47aaf33825 100644 --- a/src/hooks/DAO/loaders/useFractalNode.ts +++ b/src/hooks/DAO/loaders/useFractalNode.ts @@ -96,18 +96,22 @@ export const useFractalNode = ( currentValidSafe.current = _addressPrefix + _daoAddress; setErrorLoading(false); - let safeInfo; + let safeInfoResponseWithGuard; try { if (!safeAPI) throw new Error('SafeAPI not set'); - - safeInfo = await safeAPI.getSafeInfo(utils.getAddress(_daoAddress)); + const address = utils.getAddress(_daoAddress); + const safeInfo = await safeAPI.getSafeInfo(address); + const allTransactions = await safeAPI.getAllTransactions(address); + safeInfoResponseWithGuard = { ...safeInfo, nonceWithPending: allTransactions.count }; } catch (e) { reset({ error: true }); return; } - if (!safeInfo) { + // comment to pick up in review: will `safeInfoResponseWithGuard` ever not be set? + // typescript doesn't think so. + if (!safeInfoResponseWithGuard) { reset({ error: true }); return; } @@ -116,12 +120,12 @@ export const useFractalNode = ( action.dispatch({ type: NodeAction.SET_FRACTAL_MODULES, - payload: await lookupModules(safeInfo.modules), + payload: await lookupModules(safeInfoResponseWithGuard.modules), }); action.dispatch({ type: NodeAction.SET_SAFE_INFO, - payload: safeInfo, + payload: safeInfoResponseWithGuard, }); }, [action, lookupModules, reset, safeAPI], diff --git a/src/hooks/DAO/loaders/useLoadDAONode.ts b/src/hooks/DAO/loaders/useLoadDAONode.ts index 7be3374867..96b167cf5c 100644 --- a/src/hooks/DAO/loaders/useLoadDAONode.ts +++ b/src/hooks/DAO/loaders/useLoadDAONode.ts @@ -54,13 +54,18 @@ export const useLoadDAONode = () => { logError('graphQL query failed'); return { error: 'errorFailedSearch' }; } - const safe = await safeAPI.getSafeInfo(_daoAddress); - const fractalModules = await lookupModules(safe.modules); - const daoName = await getDaoName(utils.getAddress(safe.address), graphNodeInfo.daoName); + const safeInfo = await safeAPI.getSafeInfo(_daoAddress); + const allTransactions = await safeAPI.getAllTransactions(_daoAddress); + const safeInfoWithGuard = { ...safeInfo, nonceWithPending: allTransactions.count }; + const fractalModules = await lookupModules(safeInfo.modules); + const daoName = await getDaoName( + utils.getAddress(safeInfo.address), + graphNodeInfo.daoName, + ); const node: FractalNode = Object.assign(graphNodeInfo, { daoName, - safe, + safe: safeInfoWithGuard, fractalModules, }); diff --git a/src/types/safeGlobal.ts b/src/types/safeGlobal.ts index 60f9ff294a..621053b079 100644 --- a/src/types/safeGlobal.ts +++ b/src/types/safeGlobal.ts @@ -28,6 +28,7 @@ export declare type DataDecoded = { }; export type SafeInfoResponseWithGuard = SafeInfoResponse & { + nonceWithPending: number; guard?: string; }; From b9171bb4e791b4e012d8069122c796e933176efe Mon Sep 17 00:00:00 2001 From: Adam Gall Date: Thu, 4 Apr 2024 10:02:30 -0400 Subject: [PATCH 03/55] Use `nonceWithPending` on the ProposalCreatePage --- src/pages/daos/[daoAddress]/proposals/new/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/daos/[daoAddress]/proposals/new/index.tsx b/src/pages/daos/[daoAddress]/proposals/new/index.tsx index bd0954259a..2809e0b833 100644 --- a/src/pages/daos/[daoAddress]/proposals/new/index.tsx +++ b/src/pages/daos/[daoAddress]/proposals/new/index.tsx @@ -62,7 +62,7 @@ export default function ProposalCreatePage() { return ( validationSchema={createProposalValidation} - initialValues={{ ...DEFAULT_PROPOSAL, nonce: safe.nonce }} + initialValues={{ ...DEFAULT_PROPOSAL, nonce: safe.nonceWithPending }} onSubmit={async values => { const { nonce } = values; const proposalData = await prepareProposal(values); From cd8c7ce139884766c79a522311d7910e0fb015c1 Mon Sep 17 00:00:00 2001 From: Adam Gall Date: Thu, 4 Apr 2024 12:26:43 -0400 Subject: [PATCH 04/55] Yeah it doesn't seem like we need that check --- src/hooks/DAO/loaders/useFractalNode.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/hooks/DAO/loaders/useFractalNode.ts b/src/hooks/DAO/loaders/useFractalNode.ts index 47aaf33825..034d535d60 100644 --- a/src/hooks/DAO/loaders/useFractalNode.ts +++ b/src/hooks/DAO/loaders/useFractalNode.ts @@ -109,13 +109,6 @@ export const useFractalNode = ( return; } - // comment to pick up in review: will `safeInfoResponseWithGuard` ever not be set? - // typescript doesn't think so. - if (!safeInfoResponseWithGuard) { - reset({ error: true }); - return; - } - // if here, we have a valid Safe! action.dispatch({ From 24c44011ef2883c961c5906b457483fc57546bb6 Mon Sep 17 00:00:00 2001 From: Adam Gall Date: Thu, 4 Apr 2024 13:41:45 -0400 Subject: [PATCH 05/55] No more `nonceWithPending` Operating under the assumption that any time in the app we're utilizing the `nonce`, it's because we want to create a proposal. Under this assumption, we'll always want to take "pending transactions" into account. Also, this new code handles the cases in which: - There are no pending transactions - There are multiple pending transactions with the same nonce --- src/hooks/DAO/loaders/useFractalNode.ts | 9 +++++++-- src/hooks/DAO/loaders/useLoadDAONode.ts | 10 ++++++++-- src/pages/daos/[daoAddress]/proposals/new/index.tsx | 2 +- src/types/safeGlobal.ts | 1 - 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/hooks/DAO/loaders/useFractalNode.ts b/src/hooks/DAO/loaders/useFractalNode.ts index 034d535d60..e9c038b213 100644 --- a/src/hooks/DAO/loaders/useFractalNode.ts +++ b/src/hooks/DAO/loaders/useFractalNode.ts @@ -101,9 +101,14 @@ export const useFractalNode = ( try { if (!safeAPI) throw new Error('SafeAPI not set'); const address = utils.getAddress(_daoAddress); + const safeInfo = await safeAPI.getSafeInfo(address); - const allTransactions = await safeAPI.getAllTransactions(address); - safeInfoResponseWithGuard = { ...safeInfo, nonceWithPending: allTransactions.count }; + let nextNonce = safeInfo.nonce; + const pendingTransactions = await safeAPI.getPendingTransactions(address); + if (pendingTransactions.count > 0) { + nextNonce = Math.max(...pendingTransactions.results.map(tx => tx.nonce)) + 1; + } + safeInfoResponseWithGuard = { ...safeInfo, nonce: nextNonce }; } catch (e) { reset({ error: true }); return; diff --git a/src/hooks/DAO/loaders/useLoadDAONode.ts b/src/hooks/DAO/loaders/useLoadDAONode.ts index 96b167cf5c..1c858dfc46 100644 --- a/src/hooks/DAO/loaders/useLoadDAONode.ts +++ b/src/hooks/DAO/loaders/useLoadDAONode.ts @@ -54,9 +54,15 @@ export const useLoadDAONode = () => { logError('graphQL query failed'); return { error: 'errorFailedSearch' }; } + const safeInfo = await safeAPI.getSafeInfo(_daoAddress); - const allTransactions = await safeAPI.getAllTransactions(_daoAddress); - const safeInfoWithGuard = { ...safeInfo, nonceWithPending: allTransactions.count }; + let nextNonce = safeInfo.nonce; + const pendingTransactions = await safeAPI.getPendingTransactions(_daoAddress); + if (pendingTransactions.count > 0) { + nextNonce = Math.max(...pendingTransactions.results.map(tx => tx.nonce)) + 1; + } + const safeInfoWithGuard = { ...safeInfo, nonce: nextNonce }; + const fractalModules = await lookupModules(safeInfo.modules); const daoName = await getDaoName( utils.getAddress(safeInfo.address), diff --git a/src/pages/daos/[daoAddress]/proposals/new/index.tsx b/src/pages/daos/[daoAddress]/proposals/new/index.tsx index 2809e0b833..bd0954259a 100644 --- a/src/pages/daos/[daoAddress]/proposals/new/index.tsx +++ b/src/pages/daos/[daoAddress]/proposals/new/index.tsx @@ -62,7 +62,7 @@ export default function ProposalCreatePage() { return ( validationSchema={createProposalValidation} - initialValues={{ ...DEFAULT_PROPOSAL, nonce: safe.nonceWithPending }} + initialValues={{ ...DEFAULT_PROPOSAL, nonce: safe.nonce }} onSubmit={async values => { const { nonce } = values; const proposalData = await prepareProposal(values); diff --git a/src/types/safeGlobal.ts b/src/types/safeGlobal.ts index 621053b079..60f9ff294a 100644 --- a/src/types/safeGlobal.ts +++ b/src/types/safeGlobal.ts @@ -28,7 +28,6 @@ export declare type DataDecoded = { }; export type SafeInfoResponseWithGuard = SafeInfoResponse & { - nonceWithPending: number; guard?: string; }; From 46983832b6f966feb53e4211a2cec273db030756 Mon Sep 17 00:00:00 2001 From: Adam Gall Date: Thu, 4 Apr 2024 13:42:15 -0400 Subject: [PATCH 06/55] Found one more place in the app where we're creating a transaction but _not_ using the Safe data from global state --- src/hooks/DAO/useClawBack.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/hooks/DAO/useClawBack.ts b/src/hooks/DAO/useClawBack.ts index d5bb2ffc34..982f4c1584 100644 --- a/src/hooks/DAO/useClawBack.ts +++ b/src/hooks/DAO/useClawBack.ts @@ -25,7 +25,14 @@ export default function useClawBack({ childSafeInfo, parentAddress }: IUseClawBa const childSafeBalance = await safeAPI.getBalances( utils.getAddress(childSafeInfo.daoAddress), ); + const parentSafeInfo = await safeAPI.getSafeInfo(utils.getAddress(parentAddress)); + let parentSafeInfoNextNonce = parentSafeInfo.nonce; + const pendingTransactions = await safeAPI.getPendingTransactions(parentAddress); + if (pendingTransactions.count > 0) { + parentSafeInfoNextNonce = Math.max(...pendingTransactions.results.map(tx => tx.nonce)) + 1; + } + if (canUserCreateProposal && parentAddress && childSafeInfo && parentSafeInfo) { const abiCoder = new ethers.utils.AbiCoder(); const fractalModule = childSafeInfo.fractalModules!.find( @@ -88,7 +95,7 @@ export default function useClawBack({ childSafeInfo, parentAddress }: IUseClawBa values: transactions.map(tx => tx.value), calldatas: transactions.map(tx => tx.calldata), }, - nonce: parentSafeInfo.nonce, + nonce: parentSafeInfoNextNonce, pendingToastMessage: t('clawBackPendingToastMessage'), failedToastMessage: t('clawBackFailedToastMessage'), successToastMessage: t('clawBackSuccessToastMessage'), From b1f58001b546b3ef0588f52ce18bcfbe7760a1ca Mon Sep 17 00:00:00 2001 From: Adam Gall Date: Thu, 4 Apr 2024 13:49:16 -0400 Subject: [PATCH 07/55] Oh look there's already a function on SafeAPI to do this --- src/hooks/DAO/loaders/useFractalNode.ts | 6 +----- src/hooks/DAO/loaders/useLoadDAONode.ts | 6 +----- src/hooks/DAO/useClawBack.ts | 11 ++++------- 3 files changed, 6 insertions(+), 17 deletions(-) diff --git a/src/hooks/DAO/loaders/useFractalNode.ts b/src/hooks/DAO/loaders/useFractalNode.ts index e9c038b213..05b24f6f31 100644 --- a/src/hooks/DAO/loaders/useFractalNode.ts +++ b/src/hooks/DAO/loaders/useFractalNode.ts @@ -103,11 +103,7 @@ export const useFractalNode = ( const address = utils.getAddress(_daoAddress); const safeInfo = await safeAPI.getSafeInfo(address); - let nextNonce = safeInfo.nonce; - const pendingTransactions = await safeAPI.getPendingTransactions(address); - if (pendingTransactions.count > 0) { - nextNonce = Math.max(...pendingTransactions.results.map(tx => tx.nonce)) + 1; - } + const nextNonce = await safeAPI.getNextNonce(address); safeInfoResponseWithGuard = { ...safeInfo, nonce: nextNonce }; } catch (e) { reset({ error: true }); diff --git a/src/hooks/DAO/loaders/useLoadDAONode.ts b/src/hooks/DAO/loaders/useLoadDAONode.ts index 1c858dfc46..5f09fc15fd 100644 --- a/src/hooks/DAO/loaders/useLoadDAONode.ts +++ b/src/hooks/DAO/loaders/useLoadDAONode.ts @@ -56,11 +56,7 @@ export const useLoadDAONode = () => { } const safeInfo = await safeAPI.getSafeInfo(_daoAddress); - let nextNonce = safeInfo.nonce; - const pendingTransactions = await safeAPI.getPendingTransactions(_daoAddress); - if (pendingTransactions.count > 0) { - nextNonce = Math.max(...pendingTransactions.results.map(tx => tx.nonce)) + 1; - } + const nextNonce = await safeAPI.getNextNonce(_daoAddress); const safeInfoWithGuard = { ...safeInfo, nonce: nextNonce }; const fractalModules = await lookupModules(safeInfo.modules); diff --git a/src/hooks/DAO/useClawBack.ts b/src/hooks/DAO/useClawBack.ts index 982f4c1584..8b685623ab 100644 --- a/src/hooks/DAO/useClawBack.ts +++ b/src/hooks/DAO/useClawBack.ts @@ -26,12 +26,9 @@ export default function useClawBack({ childSafeInfo, parentAddress }: IUseClawBa utils.getAddress(childSafeInfo.daoAddress), ); - const parentSafeInfo = await safeAPI.getSafeInfo(utils.getAddress(parentAddress)); - let parentSafeInfoNextNonce = parentSafeInfo.nonce; - const pendingTransactions = await safeAPI.getPendingTransactions(parentAddress); - if (pendingTransactions.count > 0) { - parentSafeInfoNextNonce = Math.max(...pendingTransactions.results.map(tx => tx.nonce)) + 1; - } + const santitizedParentAddress = utils.getAddress(parentAddress); + const parentSafeInfo = await safeAPI.getSafeInfo(santitizedParentAddress); + const parentSafeNextNonce = await safeAPI.getNextNonce(santitizedParentAddress); if (canUserCreateProposal && parentAddress && childSafeInfo && parentSafeInfo) { const abiCoder = new ethers.utils.AbiCoder(); @@ -95,7 +92,7 @@ export default function useClawBack({ childSafeInfo, parentAddress }: IUseClawBa values: transactions.map(tx => tx.value), calldatas: transactions.map(tx => tx.calldata), }, - nonce: parentSafeInfoNextNonce, + nonce: parentSafeNextNonce, pendingToastMessage: t('clawBackPendingToastMessage'), failedToastMessage: t('clawBackFailedToastMessage'), successToastMessage: t('clawBackSuccessToastMessage'), From 2e252abf13e98fc279d3456656e45503be4133ea Mon Sep 17 00:00:00 2001 From: Adam Gall Date: Thu, 4 Apr 2024 13:54:30 -0400 Subject: [PATCH 08/55] whitespace --- src/hooks/DAO/loaders/useFractalNode.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/DAO/loaders/useFractalNode.ts b/src/hooks/DAO/loaders/useFractalNode.ts index 05b24f6f31..a1d2196768 100644 --- a/src/hooks/DAO/loaders/useFractalNode.ts +++ b/src/hooks/DAO/loaders/useFractalNode.ts @@ -100,8 +100,8 @@ export const useFractalNode = ( try { if (!safeAPI) throw new Error('SafeAPI not set'); - const address = utils.getAddress(_daoAddress); + const address = utils.getAddress(_daoAddress); const safeInfo = await safeAPI.getSafeInfo(address); const nextNonce = await safeAPI.getNextNonce(address); safeInfoResponseWithGuard = { ...safeInfo, nonce: nextNonce }; From 6fe9a419569608e3f9362cfae9abee3e1c28945d Mon Sep 17 00:00:00 2001 From: Adam Gall Date: Thu, 4 Apr 2024 13:55:37 -0400 Subject: [PATCH 09/55] variable naming --- src/hooks/DAO/loaders/useFractalNode.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/hooks/DAO/loaders/useFractalNode.ts b/src/hooks/DAO/loaders/useFractalNode.ts index a1d2196768..9337b0006a 100644 --- a/src/hooks/DAO/loaders/useFractalNode.ts +++ b/src/hooks/DAO/loaders/useFractalNode.ts @@ -96,15 +96,15 @@ export const useFractalNode = ( currentValidSafe.current = _addressPrefix + _daoAddress; setErrorLoading(false); - let safeInfoResponseWithGuard; + let safeInfo; try { if (!safeAPI) throw new Error('SafeAPI not set'); const address = utils.getAddress(_daoAddress); - const safeInfo = await safeAPI.getSafeInfo(address); + const safeInfoResponse = await safeAPI.getSafeInfo(address); const nextNonce = await safeAPI.getNextNonce(address); - safeInfoResponseWithGuard = { ...safeInfo, nonce: nextNonce }; + safeInfo = { ...safeInfoResponse, nonce: nextNonce }; } catch (e) { reset({ error: true }); return; @@ -114,12 +114,12 @@ export const useFractalNode = ( action.dispatch({ type: NodeAction.SET_FRACTAL_MODULES, - payload: await lookupModules(safeInfoResponseWithGuard.modules), + payload: await lookupModules(safeInfo.modules), }); action.dispatch({ type: NodeAction.SET_SAFE_INFO, - payload: safeInfoResponseWithGuard, + payload: safeInfo, }); }, [action, lookupModules, reset, safeAPI], From 19e9ae46339f83de050e3864105e46b83ecebd05 Mon Sep 17 00:00:00 2001 From: Adam Gall Date: Thu, 4 Apr 2024 14:01:34 -0400 Subject: [PATCH 10/55] syntax cleanup --- src/hooks/DAO/loaders/useLoadDAONode.ts | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/hooks/DAO/loaders/useLoadDAONode.ts b/src/hooks/DAO/loaders/useLoadDAONode.ts index 5f09fc15fd..a7840fc786 100644 --- a/src/hooks/DAO/loaders/useLoadDAONode.ts +++ b/src/hooks/DAO/loaders/useLoadDAONode.ts @@ -55,20 +55,15 @@ export const useLoadDAONode = () => { return { error: 'errorFailedSearch' }; } - const safeInfo = await safeAPI.getSafeInfo(_daoAddress); - const nextNonce = await safeAPI.getNextNonce(_daoAddress); + const sanitizedDaoAddress = utils.getAddress(_daoAddress); + const safeInfo = await safeAPI.getSafeInfo(sanitizedDaoAddress); + const nextNonce = await safeAPI.getNextNonce(sanitizedDaoAddress); const safeInfoWithGuard = { ...safeInfo, nonce: nextNonce }; - const fractalModules = await lookupModules(safeInfo.modules); - const daoName = await getDaoName( - utils.getAddress(safeInfo.address), - graphNodeInfo.daoName, - ); - const node: FractalNode = Object.assign(graphNodeInfo, { - daoName, + daoName: await getDaoName(sanitizedDaoAddress, graphNodeInfo.daoName), safe: safeInfoWithGuard, - fractalModules, + fractalModules: await lookupModules(safeInfo.modules), }); // TODO we could cache node here, but should be careful not to cache From 2fdb0fc5234591ab67a653750dcc179fdc1d662f Mon Sep 17 00:00:00 2001 From: Kirill Klimenko Date: Mon, 8 Apr 2024 14:29:33 +0200 Subject: [PATCH 11/55] Refactor CreateProposalTemplateForm into ProposalBuilder and make it more generic to be compatible with creating regular proposal --- .../ProposalDetails.tsx} | 10 +- .../ProposalMetadata.tsx} | 30 ++- .../ProposalTransaction.tsx} | 14 +- .../ProposalTransactions.tsx} | 19 +- .../ProposalTransactionsForm.tsx} | 24 +-- .../constants.ts | 8 +- src/components/ProposalBuilder/index.tsx | 192 ++++++++++++++++++ .../ProposalTemplateCard.tsx | 2 +- .../ui/modals/ForkProposalTemplateModal.tsx | 2 +- .../ui/modals/ProposalTemplateModal.tsx | 2 +- .../DAO/proposal/useCreateProposalTemplate.ts | 8 +- .../proposal-templates/new/index.tsx | 180 +--------------- src/providers/App/governance/action.ts | 2 +- src/types/createProposalTemplate.ts | 32 --- src/types/fractal.ts | 2 +- src/types/proposalBuilder.ts | 27 +++ 16 files changed, 287 insertions(+), 267 deletions(-) rename src/components/{CreateProposalTemplate/ProposalTemplateDetails.tsx => ProposalBuilder/ProposalDetails.tsx} (93%) rename src/components/{CreateProposalTemplate/ProposalTemplateMetadata.tsx => ProposalBuilder/ProposalMetadata.tsx} (54%) rename src/components/{CreateProposalTemplate/ProposalTemplateTransaction.tsx => ProposalBuilder/ProposalTransaction.tsx} (96%) rename src/components/{CreateProposalTemplate/ProposalTemplateTransactions.tsx => ProposalBuilder/ProposalTransactions.tsx} (86%) rename src/components/{CreateProposalTemplate/ProposalTemplateTransactionsForm.tsx => ProposalBuilder/ProposalTransactionsForm.tsx} (78%) rename src/components/{CreateProposalTemplate => ProposalBuilder}/constants.ts (52%) create mode 100644 src/components/ProposalBuilder/index.tsx delete mode 100644 src/types/createProposalTemplate.ts create mode 100644 src/types/proposalBuilder.ts diff --git a/src/components/CreateProposalTemplate/ProposalTemplateDetails.tsx b/src/components/ProposalBuilder/ProposalDetails.tsx similarity index 93% rename from src/components/CreateProposalTemplate/ProposalTemplateDetails.tsx rename to src/components/ProposalBuilder/ProposalDetails.tsx index a470459623..4bb24d63d0 100644 --- a/src/components/CreateProposalTemplate/ProposalTemplateDetails.tsx +++ b/src/components/ProposalBuilder/ProposalDetails.tsx @@ -3,7 +3,7 @@ import { FormikProps } from 'formik'; import { Fragment, PropsWithChildren } from 'react'; import { useTranslation } from 'react-i18next'; import { BACKGROUND_SEMI_TRANSPARENT } from '../../constants/common'; -import { CreateProposalTemplateForm } from '../../types/createProposalTemplate'; +import { CreateProposalForm } from '../../types/proposalBuilder'; import Markdown from '../ui/proposal/Markdown'; import '../../assets/css/Markdown.css'; @@ -29,10 +29,10 @@ export function TransactionValueContainer({ } export default function ProposalTemplateDetails({ - values: { proposalTemplateMetadata, transactions }, -}: FormikProps) { + values: { proposalMetadata, transactions }, +}: FormikProps) { const { t } = useTranslation(['proposalTemplate', 'proposal']); - const trimmedTitle = proposalTemplateMetadata.title?.trim(); + const trimmedTitle = proposalMetadata.title?.trim(); return ( {t('proposalTemplateDescription')} diff --git a/src/components/CreateProposalTemplate/ProposalTemplateMetadata.tsx b/src/components/ProposalBuilder/ProposalMetadata.tsx similarity index 54% rename from src/components/CreateProposalTemplate/ProposalTemplateMetadata.tsx rename to src/components/ProposalBuilder/ProposalMetadata.tsx index 3d58b366e3..29573b0a88 100644 --- a/src/components/CreateProposalTemplate/ProposalTemplateMetadata.tsx +++ b/src/components/ProposalBuilder/ProposalMetadata.tsx @@ -1,22 +1,20 @@ import { Button, Divider, VStack } from '@chakra-ui/react'; import { FormikProps } from 'formik'; import { useTranslation } from 'react-i18next'; -import { - CreateProposalTemplateForm, - CreateProposalTemplateFormState, -} from '../../types/createProposalTemplate'; +import { CreateProposalState } from '../../types'; +import { CreateProposalForm } from '../../types/proposalBuilder'; import { InputComponent, TextareaComponent } from '../ui/forms/InputComponent'; -export interface ProposalTemplateMetadataProps extends FormikProps { - setFormState: (state: CreateProposalTemplateFormState) => void; +export interface ProposalMetadataProps extends FormikProps { + setFormState: (state: CreateProposalState) => void; } -export default function ProposalTemplateMetadata({ - values: { proposalTemplateMetadata }, +export default function ProposalMetadata({ + values: { proposalMetadata }, setFieldValue, - errors: { proposalTemplateMetadata: proposalTemplateMetadataError }, + errors: { proposalMetadata: proposalMetadataError }, setFormState, -}: ProposalTemplateMetadataProps) { +}: ProposalMetadataProps) { const { t } = useTranslation(['proposalTemplate', 'common']); return ( @@ -30,8 +28,8 @@ export default function ProposalTemplateMetadata({ label={t('proposalTemplateTitle')} helper={t('proposalTemplateTitleHelperText')} isRequired - value={proposalTemplateMetadata.title} - onChange={e => setFieldValue('proposalTemplateMetadata.title', e.target.value)} + value={proposalMetadata.title} + onChange={e => setFieldValue('proposalMetadata.title', e.target.value)} disabled={false} testId="metadata.title" maxLength={50} @@ -41,8 +39,8 @@ export default function ProposalTemplateMetadata({ subLabel={t('')} helper={t('proposalTemplateDescriptionHelperText')} isRequired={false} - value={proposalTemplateMetadata.description} - onChange={e => setFieldValue('proposalTemplateMetadata.description', e.target.value)} + value={proposalMetadata.description} + onChange={e => setFieldValue('proposalMetadata.description', e.target.value)} disabled={false} rows={12} /> @@ -54,8 +52,8 @@ export default function ProposalTemplateMetadata({ /> diff --git a/src/components/CreateProposalTemplate/ProposalTemplateTransaction.tsx b/src/components/ProposalBuilder/ProposalTransaction.tsx similarity index 96% rename from src/components/CreateProposalTemplate/ProposalTemplateTransaction.tsx rename to src/components/ProposalBuilder/ProposalTransaction.tsx index 33e5c5bdd5..b9f83bede1 100644 --- a/src/components/CreateProposalTemplate/ProposalTemplateTransaction.tsx +++ b/src/components/ProposalBuilder/ProposalTransaction.tsx @@ -2,14 +2,14 @@ import { VStack, HStack, Text, Box, Flex, IconButton } from '@chakra-ui/react'; import { AddPlus, Minus } from '@decent-org/fractal-ui'; import { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; -import { CreateProposalTemplateTransaction } from '../../types/createProposalTemplate'; +import { CreateProposalTransaction } from '../../types/proposalBuilder'; import ABISelector, { ABIElement } from '../ui/forms/ABISelector'; import ExampleLabel from '../ui/forms/ExampleLabel'; import { BigNumberComponent, InputComponent } from '../ui/forms/InputComponent'; -import { DEFAULT_PROPOSAL_TEMPLATE_TRANSACTION } from './constants'; +import { DEFAULT_PROPOSAL_TRANSACTION } from './constants'; -interface ProposalTemplateTransactionProps { - transaction: CreateProposalTemplateTransaction; +interface ProposalTransactionProps { + transaction: CreateProposalTransaction; transactionIndex: number; transactionPending: boolean; txAddressError?: string; @@ -17,14 +17,14 @@ interface ProposalTemplateTransactionProps { setFieldValue: (field: string, value: any, shouldValidate?: boolean | undefined) => void; } -export default function ProposalTemplateTransaction({ +export default function ProposalTransaction({ transaction, transactionIndex, transactionPending, txAddressError, txFunctionError, setFieldValue, -}: ProposalTemplateTransactionProps) { +}: ProposalTransactionProps) { const { t } = useTranslation(['proposal', 'proposalTemplate', 'common']); const handleABISelectorChange = useCallback( (value: ABIElement) => { @@ -123,7 +123,7 @@ export default function ProposalTemplateTransaction({ onClick={() => setFieldValue(`transactions.${transactionIndex}.parameters`, [ ...transaction.parameters, - DEFAULT_PROPOSAL_TEMPLATE_TRANSACTION, + DEFAULT_PROPOSAL_TRANSACTION, ]) } > diff --git a/src/components/CreateProposalTemplate/ProposalTemplateTransactions.tsx b/src/components/ProposalBuilder/ProposalTransactions.tsx similarity index 86% rename from src/components/CreateProposalTemplate/ProposalTemplateTransactions.tsx rename to src/components/ProposalBuilder/ProposalTransactions.tsx index 1b4c5b4849..1f114ec7b6 100644 --- a/src/components/CreateProposalTemplate/ProposalTemplateTransactions.tsx +++ b/src/components/ProposalBuilder/ProposalTransactions.tsx @@ -12,25 +12,22 @@ import { FormikErrors, FormikProps } from 'formik'; import { Dispatch, SetStateAction } from 'react'; import { useTranslation } from 'react-i18next'; import { BigNumberValuePair } from '../../types'; -import { - CreateProposalTemplateForm, - CreateProposalTemplateTransaction, -} from '../../types/createProposalTemplate'; -import ProposalTemplateTransaction from './ProposalTemplateTransaction'; +import { CreateProposalForm, CreateProposalTransaction } from '../../types/proposalBuilder'; +import ProposalTransaction from './ProposalTransaction'; -interface ProposalTemplateTransactionsProps extends FormikProps { +interface ProposalTransactionsProps extends FormikProps { pendingTransaction: boolean; expandedIndecies: number[]; setExpandedIndecies: Dispatch>; } -export default function ProposalTemplateTransactions({ +export default function ProposalTransactions({ values: { transactions }, errors, setFieldValue, pendingTransaction, expandedIndecies, setExpandedIndecies, -}: ProposalTemplateTransactionsProps) { +}: ProposalTransactionsProps) { const { t } = useTranslation(['proposal', 'proposalTemplate', 'common']); const removeTransaction = (transactionIndex: number) => { @@ -46,7 +43,7 @@ export default function ProposalTemplateTransactions({ > {transactions.map((_, index) => { const txErrors = errors?.transactions?.[index] as - | FormikErrors> + | FormikErrors> | undefined; const txAddressError = txErrors?.targetAddress; const txFunctionError = txErrors?.functionName; @@ -101,8 +98,8 @@ export default function ProposalTemplateTransactions({ )} - { +interface ProposalTransactionsFormProps extends FormikProps { pendingTransaction: boolean; - setFormState: (state: CreateProposalTemplateFormState) => void; + setFormState: (state: CreateProposalState) => void; canUserCreateProposal?: boolean; safeNonce?: number; } -export default function ProposalTemplateTransactionsForm( - props: ProposalTemplateTransactionsFormProps, -) { +export default function ProposalTransactionsForm(props: ProposalTransactionsFormProps) { const { pendingTransaction, setFormState, @@ -41,7 +37,7 @@ export default function ProposalTemplateTransactionsForm( return ( - { - setFieldValue('transactions', [...transactions, DEFAULT_PROPOSAL_TEMPLATE_TRANSACTION]); + setFieldValue('transactions', [...transactions, DEFAULT_PROPOSAL_TRANSACTION]); setExpandedIndecies([transactions.length]); scrollToBottom(); }} @@ -85,7 +81,7 @@ export default function ProposalTemplateTransactionsForm( textStyle="text-md-mono-regular" color="gold.500" cursor="pointer" - onClick={() => setFormState(CreateProposalTemplateFormState.METADATA_FORM)} + onClick={() => setFormState(CreateProposalState.METADATA_FORM)} mb={4} > {t('back', { ns: 'common' })} diff --git a/src/components/CreateProposalTemplate/constants.ts b/src/components/ProposalBuilder/constants.ts similarity index 52% rename from src/components/CreateProposalTemplate/constants.ts rename to src/components/ProposalBuilder/constants.ts index 4c7930e184..bd76faf013 100644 --- a/src/components/CreateProposalTemplate/constants.ts +++ b/src/components/ProposalBuilder/constants.ts @@ -1,6 +1,6 @@ -import { CreateProposalTemplateTransaction } from '../../types/createProposalTemplate'; +import { CreateProposalTransaction } from '../../types/proposalBuilder'; -export const DEFAULT_PROPOSAL_TEMPLATE_TRANSACTION: CreateProposalTemplateTransaction = { +export const DEFAULT_PROPOSAL_TRANSACTION: CreateProposalTransaction = { targetAddress: '', ethValue: { value: '', bigNumberValue: undefined }, functionName: '', @@ -15,9 +15,9 @@ export const DEFAULT_PROPOSAL_TEMPLATE_TRANSACTION: CreateProposalTemplateTransa export const DEFAULT_PROPOSAL_TEMPLATE = { nonce: undefined, - proposalTemplateMetadata: { + proposalMetadata: { title: '', description: '', }, - transactions: [DEFAULT_PROPOSAL_TEMPLATE_TRANSACTION], + transactions: [DEFAULT_PROPOSAL_TRANSACTION], }; diff --git a/src/components/ProposalBuilder/index.tsx b/src/components/ProposalBuilder/index.tsx new file mode 100644 index 0000000000..590f84f6aa --- /dev/null +++ b/src/components/ProposalBuilder/index.tsx @@ -0,0 +1,192 @@ +import { Box, Flex, Grid, GridItem, Text } from '@chakra-ui/react'; +import { Trash } from '@decent-org/fractal-ui'; +import { Formik, FormikProps } from 'formik'; +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useNavigate } from 'react-router-dom'; +import { BACKGROUND_SEMI_TRANSPARENT } from '../../constants/common'; +import { DAO_ROUTES, BASE_ROUTES } from '../../constants/routes'; +import useSubmitProposal from '../../hooks/DAO/proposal/useSubmitProposal'; +import useCreateProposalTemplateSchema from '../../hooks/schemas/createProposalTemplate/useCreateProposalTemplateSchema'; +import { useCanUserCreateProposal } from '../../hooks/utils/useCanUserSubmitProposal'; +import { useFractal } from '../../providers/App/AppProvider'; +import { useNetworkConfig } from '../../providers/NetworkConfig/NetworkConfigProvider'; +import { CreateProposalState, ProposalExecuteData } from '../../types'; +import { CreateProposalForm } from '../../types/proposalBuilder'; +import { CustomNonceInput } from '../ui/forms/CustomNonceInput'; +import PageHeader from '../ui/page/Header/PageHeader'; +import ProposalDetails from './ProposalDetails'; +import ProposalMetadata from './ProposalMetadata'; +import ProposalTransactionsForm from './ProposalTransactionsForm'; +import { DEFAULT_PROPOSAL_TEMPLATE } from './constants'; + +interface IProposalBuilder { + mode: 'proposal' | 'template'; + prepareProposalData: (values: CreateProposalForm) => Promise; + initialValues: typeof DEFAULT_PROPOSAL_TEMPLATE; +} + +const templateAreaTwoCol = '"content details"'; +const templateAreaSingleCol = `"content" + "details"`; + +export default function ProposalBuilder({ mode, initialValues, prepareProposalData }: IProposalBuilder) { + const [formState, setFormState] = useState(CreateProposalState.METADATA_FORM); + const { t } = useTranslation(['proposalTemplate', 'proposal']); + + const isProposalMode = mode === 'proposal'; + + const navigate = useNavigate(); + const { + node: { daoAddress, safe }, + } = useFractal(); + const { addressPrefix } = useNetworkConfig(); + const { submitProposal, pendingCreateTx } = useSubmitProposal(); + const { canUserCreateProposal } = useCanUserCreateProposal(); + const { createProposalTemplateValidation } = useCreateProposalTemplateSchema(); + + const successCallback = () => { + if (daoAddress) { + // Redirecting to proposals page so that user will see Proposal for Proposal Template creation + navigate(DAO_ROUTES.proposals.relative(addressPrefix, daoAddress)); + } + }; + + return ( + + validationSchema={createProposalTemplateValidation} + initialValues={initialValues} + enableReinitialize + onSubmit={async values => { + if (canUserCreateProposal) { + const proposalData = await prepareProposalData(values); + if (proposalData) { + submitProposal({ + proposalData, + nonce: values?.nonce, + pendingToastMessage: t('proposalCreatePendingToastMessage', { ns: 'proposal' }), + successToastMessage: t('proposalCreateSuccessToastMessage', { ns: 'proposal' }), + failedToastMessage: t('proposalCreateFailureToastMessage', { ns: 'proposal' }), + successCallback, + }); + } + } + }} + > + {(formikProps: FormikProps) => { + const { handleSubmit } = formikProps; + + if (!daoAddress) { + return; + } + + return ( +
+ + + navigate( + daoAddress + ? isProposalMode + ? DAO_ROUTES.proposals.relative(addressPrefix, daoAddress) + : DAO_ROUTES.proposalTemplates.relative(addressPrefix, daoAddress) + : BASE_ROUTES.landing, + ) + } + isButtonDisabled={pendingCreateTx} + /> + + + + + {formState === CreateProposalState.METADATA_FORM ? ( + + ) : ( + <> + + + {formikProps.values.proposalMetadata.title} + + formikProps.setFieldValue('nonce', newNonce)} + align="end" + /> + + + + )} + + + + + + + + +
+ ); + }} + + ); +} diff --git a/src/components/ProposalTemplates/ProposalTemplateCard.tsx b/src/components/ProposalTemplates/ProposalTemplateCard.tsx index cc8336146a..fcecd49fdf 100644 --- a/src/components/ProposalTemplates/ProposalTemplateCard.tsx +++ b/src/components/ProposalTemplates/ProposalTemplateCard.tsx @@ -9,7 +9,7 @@ import useSubmitProposal from '../../hooks/DAO/proposal/useSubmitProposal'; import { useCanUserCreateProposal } from '../../hooks/utils/useCanUserSubmitProposal'; import { useFractal } from '../../providers/App/AppProvider'; import { useNetworkConfig } from '../../providers/NetworkConfig/NetworkConfigProvider'; -import { ProposalTemplate } from '../../types/createProposalTemplate'; +import { ProposalTemplate } from '../../types/proposalBuilder'; import ContentBox from '../ui/containers/ContentBox'; import { OptionMenu } from '../ui/menus/OptionMenu'; import { ModalType } from '../ui/modals/ModalProvider'; diff --git a/src/components/ui/modals/ForkProposalTemplateModal.tsx b/src/components/ui/modals/ForkProposalTemplateModal.tsx index f63cf23d7d..ccfe6c5536 100644 --- a/src/components/ui/modals/ForkProposalTemplateModal.tsx +++ b/src/components/ui/modals/ForkProposalTemplateModal.tsx @@ -9,7 +9,7 @@ import { useCanUserCreateProposal } from '../../../hooks/utils/useCanUserSubmitP import useSignerOrProvider from '../../../hooks/utils/useSignerOrProvider'; import { useFractal } from '../../../providers/App/AppProvider'; import { useNetworkConfig } from '../../../providers/NetworkConfig/NetworkConfigProvider'; -import { ProposalTemplate } from '../../../types/createProposalTemplate'; +import { ProposalTemplate } from '../../../types/proposalBuilder'; import { InputComponent } from '../forms/InputComponent'; interface IForkProposalTemplateModalProps { diff --git a/src/components/ui/modals/ProposalTemplateModal.tsx b/src/components/ui/modals/ProposalTemplateModal.tsx index 210849e214..d9250b6941 100644 --- a/src/components/ui/modals/ProposalTemplateModal.tsx +++ b/src/components/ui/modals/ProposalTemplateModal.tsx @@ -20,7 +20,7 @@ import { useCanUserCreateProposal } from '../../../hooks/utils/useCanUserSubmitP import { useFractal } from '../../../providers/App/AppProvider'; import { useNetworkConfig } from '../../../providers/NetworkConfig/NetworkConfigProvider'; import { BigNumberValuePair } from '../../../types'; -import { ProposalTemplate } from '../../../types/createProposalTemplate'; +import { ProposalTemplate } from '../../../types/proposalBuilder'; import { isValidUrl } from '../../../utils/url'; import { CustomNonceInput } from '../forms/CustomNonceInput'; import { BigNumberComponent, InputComponent } from '../forms/InputComponent'; diff --git a/src/hooks/DAO/proposal/useCreateProposalTemplate.ts b/src/hooks/DAO/proposal/useCreateProposalTemplate.ts index b804e740f2..625005cf84 100644 --- a/src/hooks/DAO/proposal/useCreateProposalTemplate.ts +++ b/src/hooks/DAO/proposal/useCreateProposalTemplate.ts @@ -3,7 +3,7 @@ import { useCallback } from 'react'; import { useFractal } from '../../../providers/App/AppProvider'; import useIPFSClient from '../../../providers/App/hooks/useIPFSClient'; import { ProposalExecuteData } from '../../../types'; -import { CreateProposalTemplateForm } from '../../../types/createProposalTemplate'; +import { CreateProposalForm } from '../../../types/proposalBuilder'; import { couldBeENS } from '../../../utils/url'; import useSafeContracts from '../../safe/useSafeContracts'; import useSignerOrProvider from '../../utils/useSignerOrProvider'; @@ -18,7 +18,7 @@ export default function useCreateProposalTemplate() { } = useFractal(); const prepareProposalTemplateProposal = useCallback( - async (values: CreateProposalTemplateForm) => { + async (values: CreateProposalForm) => { if (proposalTemplates && signerOrProvider && keyValuePairsContract) { const proposalMetadata = { title: 'Create Proposal Template', @@ -28,8 +28,8 @@ export default function useCreateProposalTemplate() { }; const proposalTemplateData = { - title: values.proposalTemplateMetadata.title.trim(), - description: values.proposalTemplateMetadata.description.trim(), + title: values.proposalMetadata.title.trim(), + description: values.proposalMetadata.description.trim(), transactions: await Promise.all( values.transactions.map(async tx => ({ ...tx, diff --git a/src/pages/daos/[daoAddress]/proposal-templates/new/index.tsx b/src/pages/daos/[daoAddress]/proposal-templates/new/index.tsx index d557d064f7..a5366c743b 100644 --- a/src/pages/daos/[daoAddress]/proposal-templates/new/index.tsx +++ b/src/pages/daos/[daoAddress]/proposal-templates/new/index.tsx @@ -1,42 +1,17 @@ -import { Box, Flex, Grid, GridItem, Text } from '@chakra-ui/react'; -import { Trash } from '@decent-org/fractal-ui'; import { BigNumber } from 'ethers'; -import { Formik, FormikProps } from 'formik'; import { useEffect, useState, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; -import { useNavigate, useSearchParams } from 'react-router-dom'; -import ProposalTemplateDetails from '../../../../../components/CreateProposalTemplate/ProposalTemplateDetails'; -import ProposalTemplateMetadata from '../../../../../components/CreateProposalTemplate/ProposalTemplateMetadata'; -import ProposalTemplateTransactionsForm from '../../../../../components/CreateProposalTemplate/ProposalTemplateTransactionsForm'; -import { DEFAULT_PROPOSAL_TEMPLATE } from '../../../../../components/CreateProposalTemplate/constants'; -import { CustomNonceInput } from '../../../../../components/ui/forms/CustomNonceInput'; -import PageHeader from '../../../../../components/ui/page/Header/PageHeader'; -import { BACKGROUND_SEMI_TRANSPARENT } from '../../../../../constants/common'; -import { BASE_ROUTES, DAO_ROUTES } from '../../../../../constants/routes'; +import { useSearchParams } from 'react-router-dom'; +import ProposalBuilder from '../../../../../components/ProposalBuilder'; +import { DEFAULT_PROPOSAL_TEMPLATE } from '../../../../../components/ProposalBuilder/constants'; import { logError } from '../../../../../helpers/errorLogging'; import useCreateProposalTemplate from '../../../../../hooks/DAO/proposal/useCreateProposalTemplate'; -import useSubmitProposal from '../../../../../hooks/DAO/proposal/useSubmitProposal'; -import useCreateProposalTemplateSchema from '../../../../../hooks/schemas/createProposalTemplate/useCreateProposalTemplateSchema'; -import { useCanUserCreateProposal } from '../../../../../hooks/utils/useCanUserSubmitProposal'; -import { useFractal } from '../../../../../providers/App/AppProvider'; import useIPFSClient from '../../../../../providers/App/hooks/useIPFSClient'; -import { useNetworkConfig } from '../../../../../providers/NetworkConfig/NetworkConfigProvider'; -import { - CreateProposalTemplateForm, - CreateProposalTemplateFormState, - ProposalTemplate, -} from '../../../../../types/createProposalTemplate'; - -const templateAreaTwoCol = '"content details"'; -const templateAreaSingleCol = `"content" - "details"`; +import { ProposalTemplate } from '../../../../../types/proposalBuilder'; export default function CreateProposalTemplatePage() { - const [formState, setFormState] = useState(CreateProposalTemplateFormState.METADATA_FORM); + const ipfsClient = useIPFSClient(); const [initialProposalTemplate, setInitialProposalTemplate] = useState(DEFAULT_PROPOSAL_TEMPLATE); - const { t } = useTranslation(['proposalTemplate', 'proposal']); - - const navigate = useNavigate(); + const { prepareProposalTemplateProposal } = useCreateProposalTemplate(); const [searchParams] = useSearchParams(); const defaultProposalTemplatesHash = useMemo( () => searchParams?.get('templatesHash'), @@ -47,24 +22,6 @@ export default function CreateProposalTemplatePage() { [searchParams], ); - const { - node: { daoAddress, safe }, - } = useFractal(); - const { addressPrefix } = useNetworkConfig(); - - const { prepareProposalTemplateProposal } = useCreateProposalTemplate(); - const { submitProposal, pendingCreateTx } = useSubmitProposal(); - const { canUserCreateProposal } = useCanUserCreateProposal(); - const { createProposalTemplateValidation } = useCreateProposalTemplateSchema(); - const ipfsClient = useIPFSClient(); - - const successCallback = () => { - if (daoAddress) { - // Redirecting to proposals page so that user will see Proposal for Proposal Template creation - navigate(DAO_ROUTES.proposals.relative(addressPrefix, daoAddress)); - } - }; - useEffect(() => { const loadInitialTemplate = async () => { if (defaultProposalTemplatesHash && defaultProposalTemplateIndex) { @@ -74,7 +31,7 @@ export default function CreateProposalTemplatePage() { if (initialTemplate) { const newInitialValue = { nonce: undefined, - proposalTemplateMetadata: { + proposalMetadata: { title: initialTemplate.title, description: initialTemplate.description || '', }, @@ -97,125 +54,10 @@ export default function CreateProposalTemplatePage() { }, [defaultProposalTemplatesHash, defaultProposalTemplateIndex, ipfsClient]); return ( - - validationSchema={createProposalTemplateValidation} + { - if (canUserCreateProposal) { - const proposalData = await prepareProposalTemplateProposal(values); - if (proposalData) { - submitProposal({ - proposalData, - nonce: values?.nonce, - pendingToastMessage: t('proposalCreatePendingToastMessage', { ns: 'proposal' }), - successToastMessage: t('proposalCreateSuccessToastMessage', { ns: 'proposal' }), - failedToastMessage: t('proposalCreateFailureToastMessage', { ns: 'proposal' }), - successCallback, - }); - } - } - }} - > - {(formikProps: FormikProps) => { - const { handleSubmit } = formikProps; - - if (!daoAddress) { - return; - } - - return ( -
- - - navigate( - daoAddress - ? DAO_ROUTES.proposalTemplates.relative(addressPrefix, daoAddress) - : BASE_ROUTES.landing, - ) - } - isButtonDisabled={pendingCreateTx} - /> - - - - - {formState === CreateProposalTemplateFormState.METADATA_FORM ? ( - - ) : ( - <> - - - {formikProps.values.proposalTemplateMetadata.title} - - formikProps.setFieldValue('nonce', newNonce)} - align="end" - /> - - - - )} - - - - - - - - -
- ); - }} - + prepareProposalData={prepareProposalTemplateProposal} + /> ); } diff --git a/src/providers/App/governance/action.ts b/src/providers/App/governance/action.ts index 0293593ea8..70b14153b6 100644 --- a/src/providers/App/governance/action.ts +++ b/src/providers/App/governance/action.ts @@ -11,7 +11,7 @@ import { GovernanceType, ERC721TokenData, } from '../../../types'; -import { ProposalTemplate } from '../../../types/createProposalTemplate'; +import { ProposalTemplate } from '../../../types/proposalBuilder'; export enum FractalGovernanceAction { SET_GOVERNANCE_TYPE = 'SET_GOVERNANCE_TYPE', diff --git a/src/types/createProposalTemplate.ts b/src/types/createProposalTemplate.ts deleted file mode 100644 index bab428f939..0000000000 --- a/src/types/createProposalTemplate.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { BigNumberValuePair } from './common'; - -export enum CreateProposalTemplateFormState { - METADATA_FORM, - TRANSACTIONS_FORM, -} - -export interface CreateProposalTemplateTransaction { - targetAddress: string; - ethValue: T; - functionName: string; - parameters: { - signature: string; - label?: string; - value?: string; - }[]; -} - -export type CreateProposalTemplateMetadata = { - title: string; - description: string; -}; - -export type CreateProposalTemplateForm = { - transactions: CreateProposalTemplateTransaction[]; - proposalTemplateMetadata: CreateProposalTemplateMetadata; - nonce?: number; -}; - -export type ProposalTemplate = { - transactions: CreateProposalTemplateTransaction[]; -} & CreateProposalTemplateMetadata; diff --git a/src/types/fractal.ts b/src/types/fractal.ts index 94223dd398..24b48dc666 100644 --- a/src/types/fractal.ts +++ b/src/types/fractal.ts @@ -35,10 +35,10 @@ import { TreasuryActions } from '../providers/App/treasury/action'; import { NodeActions } from './../providers/App/node/action'; import { ERC721TokenData, VotesTokenData } from './account'; import { ContractConnection } from './contract'; -import { ProposalTemplate } from './createProposalTemplate'; import { FreezeGuardType, FreezeVotingType } from './daoGovernance'; import { ProposalData, MultisigProposal, AzoriusProposal, SnapshotProposal } from './daoProposal'; import { TreasuryActivity } from './daoTreasury'; +import { ProposalTemplate } from './proposalBuilder'; import { AllTransfersListResponse, SafeInfoResponseWithGuard } from './safeGlobal'; import { BNFormattedPair } from './votingFungibleToken'; /** diff --git a/src/types/proposalBuilder.ts b/src/types/proposalBuilder.ts new file mode 100644 index 0000000000..8b957137e9 --- /dev/null +++ b/src/types/proposalBuilder.ts @@ -0,0 +1,27 @@ +import { BigNumberValuePair } from './common'; + +export interface CreateProposalTransaction { + targetAddress: string; + ethValue: T; + functionName: string; + parameters: { + signature: string; + label?: string; + value?: string; + }[]; +} + +export type CreateProposalMetadata = { + title: string; + description: string; +}; + +export type CreateProposalForm = { + transactions: CreateProposalTransaction[]; + proposalMetadata: CreateProposalMetadata; + nonce?: number; +}; + +export type ProposalTemplate = { + transactions: CreateProposalTransaction[]; +} & CreateProposalMetadata; From 9fd2565fb3f5eca888b80d91784980824c5a1d1c Mon Sep 17 00:00:00 2001 From: Kirill Klimenko Date: Mon, 8 Apr 2024 14:37:17 +0200 Subject: [PATCH 12/55] Prettier --- src/components/ProposalBuilder/index.tsx | 28 ++++++++++++++---------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/src/components/ProposalBuilder/index.tsx b/src/components/ProposalBuilder/index.tsx index 590f84f6aa..354dd8fd91 100644 --- a/src/components/ProposalBuilder/index.tsx +++ b/src/components/ProposalBuilder/index.tsx @@ -30,7 +30,11 @@ const templateAreaTwoCol = '"content details"'; const templateAreaSingleCol = `"content" "details"`; -export default function ProposalBuilder({ mode, initialValues, prepareProposalData }: IProposalBuilder) { +export default function ProposalBuilder({ + mode, + initialValues, + prepareProposalData, +}: IProposalBuilder) { const [formState, setFormState] = useState(CreateProposalState.METADATA_FORM); const { t } = useTranslation(['proposalTemplate', 'proposal']); @@ -59,17 +63,17 @@ export default function ProposalBuilder({ mode, initialValues, prepareProposalDa enableReinitialize onSubmit={async values => { if (canUserCreateProposal) { - const proposalData = await prepareProposalData(values); - if (proposalData) { - submitProposal({ - proposalData, - nonce: values?.nonce, - pendingToastMessage: t('proposalCreatePendingToastMessage', { ns: 'proposal' }), - successToastMessage: t('proposalCreateSuccessToastMessage', { ns: 'proposal' }), - failedToastMessage: t('proposalCreateFailureToastMessage', { ns: 'proposal' }), - successCallback, - }); - } + const proposalData = await prepareProposalData(values); + if (proposalData) { + submitProposal({ + proposalData, + nonce: values?.nonce, + pendingToastMessage: t('proposalCreatePendingToastMessage', { ns: 'proposal' }), + successToastMessage: t('proposalCreateSuccessToastMessage', { ns: 'proposal' }), + failedToastMessage: t('proposalCreateFailureToastMessage', { ns: 'proposal' }), + successCallback, + }); + } } }} > From 392eeea211f0576c50675d887ed716db6c72ab3d Mon Sep 17 00:00:00 2001 From: Kirill Klimenko Date: Mon, 8 Apr 2024 15:22:39 +0200 Subject: [PATCH 13/55] Eliminate CreateProposal and use ProposalBuilder instead --- .../ProposalBuilder/ProposalDetails.tsx | 33 ++-- .../ProposalBuilder/ProposalMetadata.tsx | 29 +++- .../ProposalBuilder/ProposalTransaction.tsx | 77 +++++---- .../ProposalBuilder/ProposalTransactions.tsx | 9 +- .../ProposalTransactionsForm.tsx | 3 +- src/components/ProposalBuilder/constants.ts | 2 +- src/components/ProposalBuilder/index.tsx | 26 +-- .../ProposalCreate/ProposalDetails.tsx | 70 -------- .../ProposalCreate/ProposalHeader.tsx | 47 ------ .../ProposalCreate/ProposalMetadata.tsx | 81 ---------- src/components/ProposalCreate/Transaction.tsx | 131 --------------- .../ProposalCreate/Transactions.tsx | 118 -------------- .../ProposalCreate/TransactionsForm.tsx | 97 ----------- src/components/ProposalCreate/constants.ts | 19 --- .../ui/modals/ProposalTemplateModal.tsx | 22 +-- .../loaders/governance/useAzoriusListeners.ts | 4 +- .../loaders/governance/useAzoriusProposals.ts | 6 +- src/hooks/DAO/proposal/useGetMetadata.ts | 20 ++- src/hooks/DAO/proposal/usePrepareProposal.ts | 16 +- src/hooks/DAO/proposal/useSubmitProposal.ts | 4 +- .../useCreateProposalSchema.ts} | 15 +- .../proposalCreate/useCreateProposalSchema.ts | 75 --------- src/i18n/locales/en/proposal.json | 2 +- .../proposal-templates/new/index.tsx | 4 +- .../daos/[daoAddress]/proposals/new/index.tsx | 152 +----------------- src/types/createProposal.ts | 27 ---- src/types/daoProposal.ts | 6 +- src/types/index.ts | 2 +- src/types/proposalBuilder.ts | 6 + 29 files changed, 178 insertions(+), 925 deletions(-) delete mode 100644 src/components/ProposalCreate/ProposalDetails.tsx delete mode 100644 src/components/ProposalCreate/ProposalHeader.tsx delete mode 100644 src/components/ProposalCreate/ProposalMetadata.tsx delete mode 100644 src/components/ProposalCreate/Transaction.tsx delete mode 100644 src/components/ProposalCreate/Transactions.tsx delete mode 100644 src/components/ProposalCreate/TransactionsForm.tsx delete mode 100644 src/components/ProposalCreate/constants.ts rename src/hooks/schemas/{createProposalTemplate/useCreateProposalTemplateSchema.ts => proposalBuilder/useCreateProposalSchema.ts} (83%) delete mode 100644 src/hooks/schemas/proposalCreate/useCreateProposalSchema.ts delete mode 100644 src/types/createProposal.ts diff --git a/src/components/ProposalBuilder/ProposalDetails.tsx b/src/components/ProposalBuilder/ProposalDetails.tsx index 4bb24d63d0..e7b917e2bd 100644 --- a/src/components/ProposalBuilder/ProposalDetails.tsx +++ b/src/components/ProposalBuilder/ProposalDetails.tsx @@ -3,7 +3,7 @@ import { FormikProps } from 'formik'; import { Fragment, PropsWithChildren } from 'react'; import { useTranslation } from 'react-i18next'; import { BACKGROUND_SEMI_TRANSPARENT } from '../../constants/common'; -import { CreateProposalForm } from '../../types/proposalBuilder'; +import { CreateProposalForm, ProposalBuilderMode } from '../../types/proposalBuilder'; import Markdown from '../ui/proposal/Markdown'; import '../../assets/css/Markdown.css'; @@ -30,7 +30,8 @@ export function TransactionValueContainer({ export default function ProposalTemplateDetails({ values: { proposalMetadata, transactions }, -}: FormikProps) { + mode, +}: FormikProps & { mode: ProposalBuilderMode }) { const { t } = useTranslation(['proposalTemplate', 'proposal']); const trimmedTitle = proposalMetadata.title?.trim(); @@ -51,19 +52,21 @@ export default function ProposalTemplateDetails({ {t('previewTitle')} {trimmedTitle} - - {t('previewThumnbail')} - {trimmedTitle && ( - title.slice(0, 2)} - /> - )} - + {mode === 'template' && ( + + {t('previewThumnbail')} + {trimmedTitle && ( + title.slice(0, 2)} + /> + )} + + )} {t('proposalTemplateDescription')} { setFormState: (state: CreateProposalState) => void; + mode: ProposalBuilderMode; } export default function ProposalMetadata({ @@ -14,8 +15,10 @@ export default function ProposalMetadata({ setFieldValue, errors: { proposalMetadata: proposalMetadataError }, setFormState, + mode, }: ProposalMetadataProps) { - const { t } = useTranslation(['proposalTemplate', 'common']); + const { t } = useTranslation(['proposalTemplate', 'proposal', 'common']); + const isProposalMode = mode === 'proposal'; return ( <> @@ -25,8 +28,14 @@ export default function ProposalMetadata({ mt={4} > setFieldValue('proposalMetadata.title', e.target.value)} @@ -35,9 +44,17 @@ export default function ProposalMetadata({ maxLength={50} /> setFieldValue('proposalMetadata.description', e.target.value)} diff --git a/src/components/ProposalBuilder/ProposalTransaction.tsx b/src/components/ProposalBuilder/ProposalTransaction.tsx index b9f83bede1..ad256b811b 100644 --- a/src/components/ProposalBuilder/ProposalTransaction.tsx +++ b/src/components/ProposalBuilder/ProposalTransaction.tsx @@ -2,7 +2,7 @@ import { VStack, HStack, Text, Box, Flex, IconButton } from '@chakra-ui/react'; import { AddPlus, Minus } from '@decent-org/fractal-ui'; import { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; -import { CreateProposalTransaction } from '../../types/proposalBuilder'; +import { CreateProposalTransaction, ProposalBuilderMode } from '../../types/proposalBuilder'; import ABISelector, { ABIElement } from '../ui/forms/ABISelector'; import ExampleLabel from '../ui/forms/ExampleLabel'; import { BigNumberComponent, InputComponent } from '../ui/forms/InputComponent'; @@ -15,6 +15,7 @@ interface ProposalTransactionProps { txAddressError?: string; txFunctionError?: string; setFieldValue: (field: string, value: any, shouldValidate?: boolean | undefined) => void; + mode: ProposalBuilderMode; } export default function ProposalTransaction({ @@ -24,7 +25,9 @@ export default function ProposalTransaction({ txAddressError, txFunctionError, setFieldValue, + mode, }: ProposalTransactionProps) { + const isProposalMode = mode === 'proposal'; const { t } = useTranslation(['proposal', 'proposalTemplate', 'common']); const handleABISelectorChange = useCallback( (value: ABIElement) => { @@ -170,34 +173,38 @@ export default function ProposalTransaction({ alignItems="center" mt={4} > - - setFieldValue( - `transactions.${transactionIndex}.parameters.${i}.label`, - e.target.value, - ) - } - disabled={transactionPending || !!parameter.value} - testId={`transactions.${transactionIndex}.parameters.${i}.label`} - subLabel={ - - {t('helperParameterLabel', { ns: 'proposalTemplate' })} - - } - gridContainerProps={{ - display: 'inline-flex', - flexWrap: 'wrap', - width: '30%', - }} - inputContainerProps={{ - width: '100%', - }} - /> - {t('or', { ns: 'common' })} + {!isProposalMode && ( + <> + + setFieldValue( + `transactions.${transactionIndex}.parameters.${i}.label`, + e.target.value, + ) + } + disabled={transactionPending || !!parameter.value} + testId={`transactions.${transactionIndex}.parameters.${i}.label`} + subLabel={ + + {t('helperParameterLabel', { ns: 'proposalTemplate' })} + + } + gridContainerProps={{ + display: 'inline-flex', + flexWrap: 'wrap', + width: '30%', + }} + inputContainerProps={{ + width: '100%', + }} + /> + {t('or', { ns: 'common' })} + + )} {t('example', { ns: 'common' })}: value - - {t('proposalTemplateLeaveBlank', { ns: 'proposalTemplate' })} - + {!isProposalMode && ( + + {t('proposalTemplateLeaveBlank', { ns: 'proposalTemplate' })} + + )} } @@ -282,7 +291,9 @@ export default function ProposalTransaction({ {`${t('example', { ns: 'common' })}:`} {'1.2'} - {t('ethParemeterHelper', { ns: 'proposalTemplate' })} + {!isProposalMode && ( + {t('ethParemeterHelper', { ns: 'proposalTemplate' })} + )} } errorMessage={undefined} diff --git a/src/components/ProposalBuilder/ProposalTransactions.tsx b/src/components/ProposalBuilder/ProposalTransactions.tsx index 1f114ec7b6..6191e73acd 100644 --- a/src/components/ProposalBuilder/ProposalTransactions.tsx +++ b/src/components/ProposalBuilder/ProposalTransactions.tsx @@ -12,13 +12,18 @@ import { FormikErrors, FormikProps } from 'formik'; import { Dispatch, SetStateAction } from 'react'; import { useTranslation } from 'react-i18next'; import { BigNumberValuePair } from '../../types'; -import { CreateProposalForm, CreateProposalTransaction } from '../../types/proposalBuilder'; +import { + CreateProposalForm, + CreateProposalTransaction, + ProposalBuilderMode, +} from '../../types/proposalBuilder'; import ProposalTransaction from './ProposalTransaction'; interface ProposalTransactionsProps extends FormikProps { pendingTransaction: boolean; expandedIndecies: number[]; setExpandedIndecies: Dispatch>; + mode: ProposalBuilderMode; } export default function ProposalTransactions({ values: { transactions }, @@ -27,6 +32,7 @@ export default function ProposalTransactions({ pendingTransaction, expandedIndecies, setExpandedIndecies, + mode, }: ProposalTransactionsProps) { const { t } = useTranslation(['proposal', 'proposalTemplate', 'common']); @@ -105,6 +111,7 @@ export default function ProposalTransactions({ transactionIndex={index} setFieldValue={setFieldValue} transactionPending={pendingTransaction} + mode={mode} />
diff --git a/src/components/ProposalBuilder/ProposalTransactionsForm.tsx b/src/components/ProposalBuilder/ProposalTransactionsForm.tsx index a79037f0b6..25e267eb00 100644 --- a/src/components/ProposalBuilder/ProposalTransactionsForm.tsx +++ b/src/components/ProposalBuilder/ProposalTransactionsForm.tsx @@ -4,7 +4,7 @@ import { FormikProps } from 'formik'; import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { CreateProposalState } from '../../types'; -import { CreateProposalForm } from '../../types/proposalBuilder'; +import { CreateProposalForm, ProposalBuilderMode } from '../../types/proposalBuilder'; import { scrollToBottom } from '../../utils/ui'; import ProposalTransactions from './ProposalTransactions'; import { DEFAULT_PROPOSAL_TRANSACTION } from './constants'; @@ -14,6 +14,7 @@ interface ProposalTransactionsFormProps extends FormikProps setFormState: (state: CreateProposalState) => void; canUserCreateProposal?: boolean; safeNonce?: number; + mode: ProposalBuilderMode; } export default function ProposalTransactionsForm(props: ProposalTransactionsFormProps) { diff --git a/src/components/ProposalBuilder/constants.ts b/src/components/ProposalBuilder/constants.ts index bd76faf013..8eb29adad4 100644 --- a/src/components/ProposalBuilder/constants.ts +++ b/src/components/ProposalBuilder/constants.ts @@ -13,7 +13,7 @@ export const DEFAULT_PROPOSAL_TRANSACTION: CreateProposalTransaction = { ], }; -export const DEFAULT_PROPOSAL_TEMPLATE = { +export const DEFAULT_PROPOSAL = { nonce: undefined, proposalMetadata: { title: '', diff --git a/src/components/ProposalBuilder/index.tsx b/src/components/ProposalBuilder/index.tsx index 354dd8fd91..93d3cbfe19 100644 --- a/src/components/ProposalBuilder/index.tsx +++ b/src/components/ProposalBuilder/index.tsx @@ -7,23 +7,22 @@ import { useNavigate } from 'react-router-dom'; import { BACKGROUND_SEMI_TRANSPARENT } from '../../constants/common'; import { DAO_ROUTES, BASE_ROUTES } from '../../constants/routes'; import useSubmitProposal from '../../hooks/DAO/proposal/useSubmitProposal'; -import useCreateProposalTemplateSchema from '../../hooks/schemas/createProposalTemplate/useCreateProposalTemplateSchema'; +import useCreateProposalSchema from '../../hooks/schemas/proposalBuilder/useCreateProposalSchema'; import { useCanUserCreateProposal } from '../../hooks/utils/useCanUserSubmitProposal'; import { useFractal } from '../../providers/App/AppProvider'; import { useNetworkConfig } from '../../providers/NetworkConfig/NetworkConfigProvider'; import { CreateProposalState, ProposalExecuteData } from '../../types'; -import { CreateProposalForm } from '../../types/proposalBuilder'; +import { CreateProposalForm, ProposalBuilderMode } from '../../types/proposalBuilder'; import { CustomNonceInput } from '../ui/forms/CustomNonceInput'; import PageHeader from '../ui/page/Header/PageHeader'; import ProposalDetails from './ProposalDetails'; import ProposalMetadata from './ProposalMetadata'; import ProposalTransactionsForm from './ProposalTransactionsForm'; -import { DEFAULT_PROPOSAL_TEMPLATE } from './constants'; interface IProposalBuilder { - mode: 'proposal' | 'template'; + mode: ProposalBuilderMode; prepareProposalData: (values: CreateProposalForm) => Promise; - initialValues: typeof DEFAULT_PROPOSAL_TEMPLATE; + initialValues: CreateProposalForm; } const templateAreaTwoCol = '"content details"'; @@ -47,7 +46,7 @@ export default function ProposalBuilder({ const { addressPrefix } = useNetworkConfig(); const { submitProposal, pendingCreateTx } = useSubmitProposal(); const { canUserCreateProposal } = useCanUserCreateProposal(); - const { createProposalTemplateValidation } = useCreateProposalTemplateSchema(); + const { createProposalValidation } = useCreateProposalSchema(); const successCallback = () => { if (daoAddress) { @@ -58,7 +57,7 @@ export default function ProposalBuilder({ return ( - validationSchema={createProposalTemplateValidation} + validationSchema={createProposalValidation} initialValues={initialValues} enableReinitialize onSubmit={async values => { @@ -88,7 +87,11 @@ export default function ProposalBuilder({
) : ( @@ -173,6 +177,7 @@ export default function ProposalBuilder({ canUserCreateProposal={canUserCreateProposal} pendingTransaction={pendingCreateTx} safeNonce={safe?.nonce} + mode={mode} {...formikProps} /> @@ -184,7 +189,10 @@ export default function ProposalBuilder({ area="details" w="100%" > - + diff --git a/src/components/ProposalCreate/ProposalDetails.tsx b/src/components/ProposalCreate/ProposalDetails.tsx deleted file mode 100644 index 2592d2f3de..0000000000 --- a/src/components/ProposalCreate/ProposalDetails.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import { Box, Divider, Flex, HStack, Text } from '@chakra-ui/react'; -import { useTranslation } from 'react-i18next'; -import { BACKGROUND_SEMI_TRANSPARENT } from '../../constants/common'; -import { useFractal } from '../../providers/App/AppProvider'; -import { AzoriusGovernance, GovernanceType } from '../../types'; -import ContentBoxTitle from '../ui/containers/ContentBox/ContentBoxTitle'; -import { BarLoader } from '../ui/loaders/BarLoader'; - -export function ProposalDetails() { - const { - node: { daoAddress, safe }, - governance, - } = useFractal(); - const { type } = governance; - const azoriusGovernance = governance as AzoriusGovernance; - const { t } = useTranslation(['proposal']); - return ( - - {!type || !daoAddress || !safe ? ( - - - - ) : ( - - {t('proposalSummaryTitle')} - - {type === GovernanceType.MULTISIG ? ( - - {t('labelProposalSigners')} - - {safe.threshold}/{safe.owners?.length} - - - ) : ( - <> - - {t('labelProposalVotingPeriod')} - {azoriusGovernance.votingStrategy?.votingPeriod?.formatted} - - - {t('labelProposalQuorum')} - - {azoriusGovernance.votingStrategy?.quorumPercentage?.formatted || - azoriusGovernance.votingStrategy?.quorumThreshold?.formatted} - - - - {t('labelProposalTimelock')} - {azoriusGovernance.votingStrategy?.timeLockPeriod?.formatted} - - - )} - - )} - - ); -} diff --git a/src/components/ProposalCreate/ProposalHeader.tsx b/src/components/ProposalCreate/ProposalHeader.tsx deleted file mode 100644 index 38983b7773..0000000000 --- a/src/components/ProposalCreate/ProposalHeader.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { HStack, Spacer, Text, Tooltip } from '@chakra-ui/react'; -import { Alert } from '@decent-org/fractal-ui'; -import { useTranslation } from 'react-i18next'; -import { TOOLTIP_MAXW } from '../../constants/common'; -import { CustomNonceInput } from '../ui/forms/CustomNonceInput'; - -export function ProposalHeader({ - isAzorius, - metadataTitle, - nonce, - setNonce, -}: { - isAzorius?: boolean; - metadataTitle?: string; - nonce?: number; - setNonce: (nonce?: number) => void; -}) { - const { t } = useTranslation('proposal'); - - return ( - - - {metadataTitle ? metadataTitle : t('proposal', { ns: 'proposal' })} - - {!isAzorius && ( - - - - )} - - {!isAzorius && ( - - )} - - ); -} diff --git a/src/components/ProposalCreate/ProposalMetadata.tsx b/src/components/ProposalCreate/ProposalMetadata.tsx deleted file mode 100644 index a25642fc5c..0000000000 --- a/src/components/ProposalCreate/ProposalMetadata.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import { Button, Divider, VStack } from '@chakra-ui/react'; -import { FormikProps } from 'formik'; -import { useTranslation } from 'react-i18next'; -import { CreateProposalForm, CreateProposalState } from '../../types'; -import { InputComponent, TextareaComponent } from '../ui/forms/InputComponent'; - -export interface AzoriusMetadataProps extends FormikProps { - isVisible: boolean; - setFormState: (state: CreateProposalState) => void; -} - -function ProposalMetadata(props: AzoriusMetadataProps) { - const { - values: { proposalMetadata }, - setFieldValue, - errors: { proposalMetadata: proposalMetadataError }, - isVisible, - setFormState, - } = props; - const { t } = useTranslation(['proposal', 'common']); - - if (!isVisible) return null; - - return ( - <> - - setFieldValue('proposalMetadata.title', e.target.value)} - disabled={false} - placeholder={t('proposalTitlePlaceholder')} - testId="metadata.title" - /> - setFieldValue('proposalMetadata.description', e.target.value)} - disabled={false} - placeholder={t('proposalDescriptionPlaceholder')} - rows={9} - /> - setFieldValue('proposalMetadata.documentationUrl', e.target.value)} - value={proposalMetadata.documentationUrl} - disabled={false} - placeholder={t('proposalAdditionalResourcesPlaceholder')} - errorMessage={ - proposalMetadata.documentationUrl && proposalMetadataError?.documentationUrl - } - testId="metadata.documentationUrl" - /> - - - - - ); -} - -export default ProposalMetadata; diff --git a/src/components/ProposalCreate/Transaction.tsx b/src/components/ProposalCreate/Transaction.tsx deleted file mode 100644 index 481328f087..0000000000 --- a/src/components/ProposalCreate/Transaction.tsx +++ /dev/null @@ -1,131 +0,0 @@ -import { VStack, HStack, Text } from '@chakra-ui/react'; -import { useTranslation } from 'react-i18next'; -import { CreateProposalTransaction } from '../../types/createProposal'; -import ExampleLabel from '../ui/forms/ExampleLabel'; -import { BigNumberComponent, InputComponent } from '../ui/forms/InputComponent'; - -interface TransactionProps { - transaction: CreateProposalTransaction; - transactionIndex: number; - transactionPending: boolean; - txAddressError?: string; - txFunctionError?: string; - setFieldValue: (field: string, value: any, shouldValidate?: boolean | undefined) => void; -} - -function Transaction({ - transaction, - transactionIndex, - transactionPending, - txAddressError, - txFunctionError, - setFieldValue, -}: TransactionProps) { - const { t } = useTranslation(['proposal', 'common']); - - return ( - - - {`${t('example', { ns: 'common' })}:`} - 0x4168592... - - } - errorMessage={transaction.targetAddress && txAddressError ? txAddressError : undefined} - value={transaction.targetAddress} - testId="transaction.targetAddress" - onChange={e => - setFieldValue(`transactions.${transactionIndex}.targetAddress`, e.target.value) - } - /> - - - setFieldValue(`transactions.${transactionIndex}.functionName`, e.target.value) - } - disabled={transactionPending} - subLabel={ - - {`${t('example', { ns: 'common' })}:`} - transfer - - } - // @todo update withn new error messages - errorMessage={undefined} - testId="transaction.functionName" - /> - - setFieldValue(`transactions.${transactionIndex}.functionSignature`, e.target.value) - } - disabled={transactionPending} - subLabel={ - - {`${t('example', { ns: 'common' })}:`} - address, uint256 - - } - testId="transaction.functionSignature" - errorMessage={ - transaction.functionSignature && txFunctionError ? txFunctionError : undefined - } - /> - setFieldValue(`transactions.${transactionIndex}.parameters`, e.target.value)} - disabled={transactionPending} - subLabel={ - - {`${t('example', { ns: 'common' })}:`} - - {'0xADC74eE329a23060d3CB431Be0AB313740c191E7, 1000000000'} - - - } - testId="transaction.parameters" - errorMessage={transaction.parameters && txFunctionError ? txFunctionError : undefined} - /> - - - {`${t('example', { ns: 'common' })}:`} - {'1.2'} - - } - errorMessage={undefined} - value={transaction.ethValue.bigNumberValue} - onChange={e => { - setFieldValue(`transactions.${transactionIndex}.ethValue`, e); - }} - decimalPlaces={18} - /> - - ); -} - -export default Transaction; diff --git a/src/components/ProposalCreate/Transactions.tsx b/src/components/ProposalCreate/Transactions.tsx deleted file mode 100644 index 29207f54e5..0000000000 --- a/src/components/ProposalCreate/Transactions.tsx +++ /dev/null @@ -1,118 +0,0 @@ -import { - Box, - Accordion, - AccordionButton, - AccordionItem, - AccordionPanel, - HStack, - IconButton, -} from '@chakra-ui/react'; -import { ArrowDown, ArrowRight, Minus } from '@decent-org/fractal-ui'; -import { FormikErrors, FormikProps } from 'formik'; -import { Dispatch, SetStateAction } from 'react'; -import { useTranslation } from 'react-i18next'; -import { BigNumberValuePair, CreateProposalForm, CreateProposalTransaction } from '../../types'; -import Transaction from './Transaction'; - -interface TransactionsProps extends FormikProps { - isVisible: boolean; - pendingTransaction: boolean; - expandedIndecies: number[]; - setExpandedIndecies: Dispatch>; -} -function Transactions({ - values: { transactions }, - errors, - setFieldValue, - pendingTransaction, - expandedIndecies, - setExpandedIndecies, -}: TransactionsProps) { - const { t } = useTranslation(['proposal', 'common']); - - const removeTransaction = (transactionIndex: number) => { - const transactionsArr = [...transactions]; - transactionsArr.splice(transactionIndex, 1); - setFieldValue('transactions', transactionsArr); - }; - return ( - - {transactions.map((_, index) => { - const txErrors = errors?.transactions?.[index] as - | FormikErrors> - | undefined; - const txAddressError = txErrors?.targetAddress; - const txFunctionError = txErrors?.encodedFunctionData; - - return ( - - {({ isExpanded }) => ( - - - { - setExpandedIndecies(indexArray => { - if (indexArray.includes(index)) { - const newTxArr = [...indexArray]; - newTxArr.splice(newTxArr.indexOf(index), 1); - return newTxArr; - } else { - return [...indexArray, index]; - } - }); - }} - p={0} - textStyle="text-button-md-semibold" - color="grayscale.100" - > - {isExpanded ? : } - {t('transaction')} {index + 1} - - {index !== 0 || transactions.length !== 1 ? ( - } - aria-label={t('removetransactionlabel')} - variant="unstyled" - onClick={() => removeTransaction(index)} - minWidth="auto" - _hover={{ color: 'gold.500' }} - _disabled={{ opacity: 0.4, cursor: 'default' }} - sx={{ '&:disabled:hover': { color: 'inherit', opacity: 0.4 } }} - isDisabled={pendingTransaction} - /> - ) : ( - - )} - - - - - - )} - - ); - })} - - ); -} - -export default Transactions; diff --git a/src/components/ProposalCreate/TransactionsForm.tsx b/src/components/ProposalCreate/TransactionsForm.tsx deleted file mode 100644 index 43d83bd404..0000000000 --- a/src/components/ProposalCreate/TransactionsForm.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import { Button, Box, Flex, Text, VStack, Divider, Alert, AlertTitle } from '@chakra-ui/react'; -import { Info } from '@decent-org/fractal-ui'; -import { FormikProps } from 'formik'; -import { useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { CreateProposalForm, CreateProposalState } from '../../types'; -import { scrollToBottom } from '../../utils/ui'; -import Transactions from './Transactions'; -import { DEFAULT_TRANSACTION } from './constants'; - -interface TransactionsFormProps extends FormikProps { - isVisible: boolean; - pendingTransaction: boolean; - setFormState: (state: CreateProposalState) => void; - canUserCreateProposal?: boolean; -} - -function TransactionsForm(props: TransactionsFormProps) { - const { - isVisible, - pendingTransaction, - setFormState, - setFieldValue, - values: { transactions }, - errors: { transactions: transactionsError }, - canUserCreateProposal, - } = props; - const { t } = useTranslation(['proposal', 'common']); - const [expandedIndecies, setExpandedIndecies] = useState([0]); - - if (!isVisible) return null; - - return ( - - - - - - - - - {t('transactionExecutionAlertMessage')} - - - - - - - - - - - ); -} - -export default TransactionsForm; diff --git a/src/components/ProposalCreate/constants.ts b/src/components/ProposalCreate/constants.ts deleted file mode 100644 index 6c8c3a7486..0000000000 --- a/src/components/ProposalCreate/constants.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { BigNumber } from 'ethers'; -export const DEFAULT_TRANSACTION = { - targetAddress: '', - ethValue: { value: '0', bigNumberValue: BigNumber.from('0') }, - functionName: '', - functionSignature: '', - parameters: '', - encodedFunctionData: undefined, -}; - -export const DEFAULT_PROPOSAL = { - proposalMetadata: { - title: '', - description: '', - documentationUrl: '', - }, - transactions: [DEFAULT_TRANSACTION], - nonce: 0, -}; diff --git a/src/components/ui/modals/ProposalTemplateModal.tsx b/src/components/ui/modals/ProposalTemplateModal.tsx index d9250b6941..a96b227aa5 100644 --- a/src/components/ui/modals/ProposalTemplateModal.tsx +++ b/src/components/ui/modals/ProposalTemplateModal.tsx @@ -8,7 +8,7 @@ import { Switch, VStack, } from '@chakra-ui/react'; -import { utils, BigNumber } from 'ethers'; +import { BigNumber } from 'ethers'; import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; @@ -21,7 +21,6 @@ import { useFractal } from '../../../providers/App/AppProvider'; import { useNetworkConfig } from '../../../providers/NetworkConfig/NetworkConfigProvider'; import { BigNumberValuePair } from '../../../types'; import { ProposalTemplate } from '../../../types/proposalBuilder'; -import { isValidUrl } from '../../../utils/url'; import { CustomNonceInput } from '../forms/CustomNonceInput'; import { BigNumberComponent, InputComponent } from '../forms/InputComponent'; import Markdown from '../proposal/Markdown'; @@ -115,27 +114,10 @@ export default function ProposalTemplateModal({ documentationUrl: '', }; - const proposalTransactions = filledProposalTransactions.map( - ({ targetAddress, ethValue, functionName, parameters }) => { - return { - targetAddress: utils.getAddress(targetAddress), // Safe proposal creation/execution might fail if targetAddress is not checksummed - ethValue, - functionName, - functionSignature: parameters.map(parameter => parameter.signature.trim()).join(', '), - parameters: parameters - .map(parameter => - isValidUrl(parameter.value!.trim()) - ? encodeURIComponent(parameter.value!.trim()) // If parameter.value is valid URL with special symbols like ":" or "?" - decoding might fail, thus we need to encode URL - : parameter.value!.trim(), - ) - .join(', '), - }; - }, - ); try { const proposalData = await prepareProposal({ proposalMetadata, - transactions: proposalTransactions, + transactions: filledProposalTransactions, }); submitProposal({ diff --git a/src/hooks/DAO/loaders/governance/useAzoriusListeners.ts b/src/hooks/DAO/loaders/governance/useAzoriusListeners.ts index 291a938e46..8784feeeda 100644 --- a/src/hooks/DAO/loaders/governance/useAzoriusListeners.ts +++ b/src/hooks/DAO/loaders/governance/useAzoriusListeners.ts @@ -13,7 +13,7 @@ import { useFractal } from '../../../../providers/App/AppProvider'; import { FractalGovernanceAction } from '../../../../providers/App/governance/action'; import { useEthersProvider } from '../../../../providers/Ethers/hooks/useEthersProvider'; import { - ProposalMetadata, + CreateProposalMetadata, VotingStrategyType, DecodedTransaction, FractalActions, @@ -49,7 +49,7 @@ const proposalCreatedEventListener = ( return; } - const metaDataEvent: ProposalMetadata = JSON.parse(metadata); + const metaDataEvent: CreateProposalMetadata = JSON.parse(metadata); const proposalData = { metaData: { title: metaDataEvent.title, diff --git a/src/hooks/DAO/loaders/governance/useAzoriusProposals.ts b/src/hooks/DAO/loaders/governance/useAzoriusProposals.ts index 1e8b61d0f5..3988776699 100644 --- a/src/hooks/DAO/loaders/governance/useAzoriusProposals.ts +++ b/src/hooks/DAO/loaders/governance/useAzoriusProposals.ts @@ -8,7 +8,7 @@ import { VotedEvent as ERC721VotedEvent } from '@fractal-framework/fractal-contr import { useCallback, useEffect, useMemo, useRef } from 'react'; import { useFractal } from '../../../../providers/App/AppProvider'; import { useEthersProvider } from '../../../../providers/Ethers/hooks/useEthersProvider'; -import { ProposalMetadata, VotingStrategyType, DecodedTransaction } from '../../../../types'; +import { CreateProposalMetadata, VotingStrategyType, DecodedTransaction } from '../../../../types'; import { AzoriusProposal } from '../../../../types/daoProposal'; import { Providers } from '../../../../types/network'; import { mapProposalCreatedEventToProposal, decodeTransactions } from '../../../../utils'; @@ -140,7 +140,9 @@ export const useAzoriusProposals = () => { for (const proposalCreatedEvent of proposalCreatedEvents) { let proposalData; if (proposalCreatedEvent.args.metadata) { - const metadataEvent: ProposalMetadata = JSON.parse(proposalCreatedEvent.args.metadata); + const metadataEvent: CreateProposalMetadata = JSON.parse( + proposalCreatedEvent.args.metadata, + ); const decodedTransactions = await decodeTransactions( _decode, proposalCreatedEvent.args.transactions, diff --git a/src/hooks/DAO/proposal/useGetMetadata.ts b/src/hooks/DAO/proposal/useGetMetadata.ts index edcc6975c9..6a80aeee06 100644 --- a/src/hooks/DAO/proposal/useGetMetadata.ts +++ b/src/hooks/DAO/proposal/useGetMetadata.ts @@ -2,7 +2,11 @@ import { utils } from 'ethers'; import { useCallback, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import useIPFSClient from '../../../providers/App/hooks/useIPFSClient'; -import { FractalProposal, ProposalMetadata, SafeMultisigTransactionResponse } from '../../../types'; +import { + FractalProposal, + CreateProposalMetadata, + SafeMultisigTransactionResponse, +} from '../../../types'; import { CacheKeys } from '../../utils/cache/cacheDefaults'; import { DBObjectKeys, useIndexedDB } from '../../utils/cache/useLocalDB'; @@ -20,15 +24,15 @@ interface Transaction { const useGetMultisigMetadata = (proposal: FractalProposal | null | undefined) => { const ipfsClient = useIPFSClient(); - const [multisigMetadata, setMultisigMetadata] = useState( - undefined, - ); + const [multisigMetadata, setMultisigMetadata] = useState< + undefined | CreateProposalMetadata | null + >(undefined); const [setValue, getValue] = useIndexedDB(DBObjectKeys.DECODED_TRANSACTIONS); const fetchMultisigMetadata = useCallback(async () => { if (!proposal) return; - const cached: ProposalMetadata = await getValue( + const cached: CreateProposalMetadata = await getValue( CacheKeys.MULTISIG_METADATA_PREFIX + proposal.proposalId, ); if (cached) { @@ -59,7 +63,7 @@ const useGetMultisigMetadata = (proposal: FractalProposal | null | undefined) => try { const decoded = new utils.AbiCoder().decode(['string'], encodedMetadata); const ipfsHash = (decoded as string[])[0]; - const meta: ProposalMetadata = await ipfsClient.cat(ipfsHash); + const meta: CreateProposalMetadata = await ipfsClient.cat(ipfsHash); // cache the metadata JSON await setValue(CacheKeys.MULTISIG_METADATA_PREFIX + proposal.proposalId, meta); @@ -78,7 +82,9 @@ const useGetMultisigMetadata = (proposal: FractalProposal | null | undefined) => return { multisigMetadata }; }; -export const useGetMetadata = (proposal: FractalProposal | null | undefined): ProposalMetadata => { +export const useGetMetadata = ( + proposal: FractalProposal | null | undefined, +): CreateProposalMetadata => { const { multisigMetadata } = useGetMultisigMetadata(proposal); const { t } = useTranslation('dashboard'); diff --git a/src/hooks/DAO/proposal/usePrepareProposal.ts b/src/hooks/DAO/proposal/usePrepareProposal.ts index c9914cd0b3..e81a71463c 100644 --- a/src/hooks/DAO/proposal/usePrepareProposal.ts +++ b/src/hooks/DAO/proposal/usePrepareProposal.ts @@ -1,8 +1,8 @@ import { useCallback } from 'react'; import { useEthersSigner } from '../../../providers/Ethers/hooks/useEthersSigner'; -import { CreateProposalForm } from '../../../types'; +import { CreateProposalForm } from '../../../types/proposalBuilder'; import { encodeFunction } from '../../../utils/crypto'; -import { couldBeENS } from '../../../utils/url'; +import { couldBeENS, isValidUrl } from '../../../utils/url'; export function usePrepareProposal() { const signer = useEthersSigner(); @@ -12,7 +12,17 @@ export function usePrepareProposal() { const transactionsWithEncoding = transactions.map(tx => { return { ...tx, - encodedFunctionData: encodeFunction(tx.functionName, tx.functionSignature, tx.parameters), + encodedFunctionData: encodeFunction( + tx.functionName, + tx.parameters.map(parameter => parameter.signature.trim()).join(', '), + tx.parameters + .map(parameter => + isValidUrl(parameter.value!.trim()) + ? encodeURIComponent(parameter.value!.trim()) // If parameter.value is valid URL with special symbols like ":" or "?" - decoding might fail, thus we need to encode URL + : parameter.value!.trim(), + ) + .join(', '), + ), }; }); const targets = await Promise.all( diff --git a/src/hooks/DAO/proposal/useSubmitProposal.ts b/src/hooks/DAO/proposal/useSubmitProposal.ts index e4fa5a562e..fe24c2151c 100644 --- a/src/hooks/DAO/proposal/useSubmitProposal.ts +++ b/src/hooks/DAO/proposal/useSubmitProposal.ts @@ -15,7 +15,7 @@ import { useSafeAPI } from '../../../providers/App/hooks/useSafeAPI'; import { useEthersProvider } from '../../../providers/Ethers/hooks/useEthersProvider'; import { useEthersSigner } from '../../../providers/Ethers/hooks/useEthersSigner'; import { useNetworkConfig } from '../../../providers/NetworkConfig/NetworkConfigProvider'; -import { MetaTransaction, ProposalExecuteData, ProposalMetadata } from '../../../types'; +import { MetaTransaction, ProposalExecuteData, CreateProposalMetadata } from '../../../types'; import { buildSafeApiUrl, getAzoriusModuleFromModules } from '../../../utils'; import useSafeContracts from '../../safe/useSafeContracts'; import useSignerOrProvider from '../../utils/useSignerOrProvider'; @@ -116,7 +116,7 @@ export default function useSubmitProposal() { proposalData.metaData.description || proposalData.metaData.documentationUrl ) { - const metaData: ProposalMetadata = { + const metaData: CreateProposalMetadata = { title: proposalData.metaData.title || '', description: proposalData.metaData.description || '', documentationUrl: proposalData.metaData.documentationUrl || '', diff --git a/src/hooks/schemas/createProposalTemplate/useCreateProposalTemplateSchema.ts b/src/hooks/schemas/proposalBuilder/useCreateProposalSchema.ts similarity index 83% rename from src/hooks/schemas/createProposalTemplate/useCreateProposalTemplateSchema.ts rename to src/hooks/schemas/proposalBuilder/useCreateProposalSchema.ts index 9da83d62af..cb7b9a92ae 100644 --- a/src/hooks/schemas/createProposalTemplate/useCreateProposalTemplateSchema.ts +++ b/src/hooks/schemas/proposalBuilder/useCreateProposalSchema.ts @@ -5,10 +5,10 @@ import { useFractal } from '../../../providers/App/AppProvider'; import { useValidationAddress } from '../common/useValidationAddress'; /** - * validation schema for Create Proposal Template workflow + * validation schema for Create Proposal workflow * @dev https://www.npmjs.com/package/yup */ -const useCreateProposalTemplateSchema = () => { +const useCreateProposalSchema = () => { const { t } = useTranslation('proposal'); const { addressValidationTest } = useValidationAddress(); const { @@ -30,7 +30,7 @@ const useCreateProposalTemplateSchema = () => { return false; }; - const createProposalTemplateValidation = useMemo( + const createProposalValidation = useMemo( () => Yup.object().shape({ transactions: Yup.array() @@ -59,9 +59,10 @@ const useCreateProposalTemplateSchema = () => { ), }), ), - proposalTemplateMetadata: Yup.object().shape({ + proposalMetadata: Yup.object().shape({ title: Yup.string().trim().required().max(50), - description: Yup.string().trim().notRequired().max(300), + description: Yup.string().trim().notRequired(), + documentationUrl: Yup.string().trim().notRequired(), }), nonce: Yup.number() .required() @@ -69,7 +70,7 @@ const useCreateProposalTemplateSchema = () => { }), [addressValidationTest, t, safe], ); - return { createProposalTemplateValidation }; + return { createProposalValidation }; }; -export default useCreateProposalTemplateSchema; +export default useCreateProposalSchema; diff --git a/src/hooks/schemas/proposalCreate/useCreateProposalSchema.ts b/src/hooks/schemas/proposalCreate/useCreateProposalSchema.ts deleted file mode 100644 index 7389278cd8..0000000000 --- a/src/hooks/schemas/proposalCreate/useCreateProposalSchema.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; -import * as Yup from 'yup'; -import { encodeFunction } from '../../../utils/crypto'; -import { useValidationAddress } from '../common/useValidationAddress'; - -/** - * validation schema for Create Proposal workflow - * @dev https://www.npmjs.com/package/yup - */ -export const useCreateProposalSchema = () => { - const { addressValidationTest } = useValidationAddress(); - - const { t } = useTranslation('proposal'); - - const createProposalValidation = useMemo( - () => - Yup.object().shape({ - transactions: Yup.array() - .min(1) - .of( - Yup.object().shape({ - targetAddress: Yup.string().required().test(addressValidationTest), - ethValue: Yup.object().shape({ - value: Yup.string(), - }), - functionName: Yup.string().matches(/^[a-z0-9]+$/i, { - message: t('functionNameError'), - }), - functionSignature: Yup.string(), - parameters: Yup.string(), - encodedFunctionData: Yup.string().test({ - message: t('errorInvalidFragments'), - test: (_, context) => { - const functionName = context.parent.functionName; - const functionSignature = context.parent.functionSignature; - const parameters = context.parent.parameters; - if (!functionName) return false; - const encodedFunction = encodeFunction( - functionName, - functionSignature, - parameters, - ); - - return !!encodedFunction; - }, - }), - }), - ), - proposalMetadata: Yup.object().shape({ - title: Yup.string().notRequired(), - description: Yup.string().notRequired(), - documentationUrl: Yup.string() - .notRequired() - .when({ - is: (value?: string) => !!value, - then: schema => - schema.test({ - message: t('Invalid URL'), - test: value => { - try { - return Boolean(new URL(value || '')); - } catch (e) { - return false; - } - }, - }), - }), - }), - nonce: Yup.number(), - }), - [addressValidationTest, t], - ); - return { createProposalValidation }; -}; diff --git a/src/i18n/locales/en/proposal.json b/src/i18n/locales/en/proposal.json index 2f8da0fd37..c01bebca56 100644 --- a/src/i18n/locales/en/proposal.json +++ b/src/i18n/locales/en/proposal.json @@ -2,7 +2,7 @@ "proposalOverview": "Proposal Overview", "breakdownTitle": "Breakdown", "labelTargetAddress": "Target Address", - "helperTargetAddress": "The smart contract address this proposal will modify", + "helperTargetAddress": "The smart contract address this proposal will modify. Paste address and we'll try to fetch ABI for this contract from Etherscan", "labelFunctionName": "Function Name", "helperFunctionName": "The name of the function to be called if this proposal passes", "labelFunctionSignature": "Function Signature", diff --git a/src/pages/daos/[daoAddress]/proposal-templates/new/index.tsx b/src/pages/daos/[daoAddress]/proposal-templates/new/index.tsx index a5366c743b..4ccb364cc3 100644 --- a/src/pages/daos/[daoAddress]/proposal-templates/new/index.tsx +++ b/src/pages/daos/[daoAddress]/proposal-templates/new/index.tsx @@ -2,7 +2,7 @@ import { BigNumber } from 'ethers'; import { useEffect, useState, useMemo } from 'react'; import { useSearchParams } from 'react-router-dom'; import ProposalBuilder from '../../../../../components/ProposalBuilder'; -import { DEFAULT_PROPOSAL_TEMPLATE } from '../../../../../components/ProposalBuilder/constants'; +import { DEFAULT_PROPOSAL } from '../../../../../components/ProposalBuilder/constants'; import { logError } from '../../../../../helpers/errorLogging'; import useCreateProposalTemplate from '../../../../../hooks/DAO/proposal/useCreateProposalTemplate'; import useIPFSClient from '../../../../../providers/App/hooks/useIPFSClient'; @@ -10,7 +10,7 @@ import { ProposalTemplate } from '../../../../../types/proposalBuilder'; export default function CreateProposalTemplatePage() { const ipfsClient = useIPFSClient(); - const [initialProposalTemplate, setInitialProposalTemplate] = useState(DEFAULT_PROPOSAL_TEMPLATE); + const [initialProposalTemplate, setInitialProposalTemplate] = useState(DEFAULT_PROPOSAL); const { prepareProposalTemplateProposal } = useCreateProposalTemplate(); const [searchParams] = useSearchParams(); const defaultProposalTemplatesHash = useMemo( diff --git a/src/pages/daos/[daoAddress]/proposals/new/index.tsx b/src/pages/daos/[daoAddress]/proposals/new/index.tsx index bd0954259a..7a33cb0a6d 100644 --- a/src/pages/daos/[daoAddress]/proposals/new/index.tsx +++ b/src/pages/daos/[daoAddress]/proposals/new/index.tsx @@ -1,55 +1,17 @@ -import { Grid, GridItem, Box, Flex, Center } from '@chakra-ui/react'; -import { Trash } from '@decent-org/fractal-ui'; -import { Formik, FormikProps } from 'formik'; -import { useState, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; -import { useNavigate } from 'react-router-dom'; -import { ProposalDetails } from '../../../../../components/ProposalCreate/ProposalDetails'; -import { ProposalHeader } from '../../../../../components/ProposalCreate/ProposalHeader'; -import ProposalMetadata from '../../../../../components/ProposalCreate/ProposalMetadata'; -import TransactionsForm from '../../../../../components/ProposalCreate/TransactionsForm'; -import { DEFAULT_PROPOSAL } from '../../../../../components/ProposalCreate/constants'; +import { Center } from '@chakra-ui/react'; +import ProposalBuilder from '../../../../../components/ProposalBuilder'; +import { DEFAULT_PROPOSAL } from '../../../../../components/ProposalBuilder/constants'; import { BarLoader } from '../../../../../components/ui/loaders/BarLoader'; -import PageHeader from '../../../../../components/ui/page/Header/PageHeader'; -import { BACKGROUND_SEMI_TRANSPARENT, HEADER_HEIGHT } from '../../../../../constants/common'; -import { DAO_ROUTES } from '../../../../../constants/routes'; +import { HEADER_HEIGHT } from '../../../../../constants/common'; import { usePrepareProposal } from '../../../../../hooks/DAO/proposal/usePrepareProposal'; -import useSubmitProposal from '../../../../../hooks/DAO/proposal/useSubmitProposal'; -import { useCreateProposalSchema } from '../../../../../hooks/schemas/proposalCreate/useCreateProposalSchema'; -import { useCanUserCreateProposal } from '../../../../../hooks/utils/useCanUserSubmitProposal'; import { useFractal } from '../../../../../providers/App/AppProvider'; -import { useNetworkConfig } from '../../../../../providers/NetworkConfig/NetworkConfigProvider'; -import { CreateProposalForm, CreateProposalState, GovernanceType } from '../../../../../types'; - -const templateAreaTwoCol = '"content details"'; -const templateAreaSingleCol = `"content" - "details"`; export default function ProposalCreatePage() { const { node: { daoAddress, safe }, governance: { type }, } = useFractal(); - const { addressPrefix } = useNetworkConfig(); - const { createProposalValidation } = useCreateProposalSchema(); const { prepareProposal } = usePrepareProposal(); - const { submitProposal, pendingCreateTx } = useSubmitProposal(); - const { canUserCreateProposal } = useCanUserCreateProposal(); - - const navigate = useNavigate(); - const { t } = useTranslation(['proposal', 'common', 'breadcrumbs']); - - const [formState, setFormState] = useState(CreateProposalState.METADATA_FORM); - const isAzorius = useMemo( - () => type === GovernanceType.AZORIUS_ERC20 || type === GovernanceType.AZORIUS_ERC721, - [type], - ); - - const successCallback = () => { - if (daoAddress) { - navigate(DAO_ROUTES.proposals.relative(addressPrefix, daoAddress)); - } - }; if (!type || !daoAddress || !safe) { return ( @@ -60,108 +22,10 @@ export default function ProposalCreatePage() { } return ( - - validationSchema={createProposalValidation} + { - const { nonce } = values; - const proposalData = await prepareProposal(values); - submitProposal({ - proposalData, - nonce, - pendingToastMessage: t('proposalCreatePendingToastMessage'), - successToastMessage: t('proposalCreateSuccessToastMessage'), - failedToastMessage: t('proposalCreateFailureToastMessage'), - successCallback, - }); - }} - validateOnMount - isInitialValid={false} - > - {(formikProps: FormikProps) => { - const { handleSubmit, setFieldValue, values } = formikProps; - return ( - - - - navigate(DAO_ROUTES.proposals.relative(addressPrefix, daoAddress)) - } - isButtonDisabled={pendingCreateTx} - /> - - - - - { - setFieldValue('nonce', nonce); - }} - /> - - - - - - - - - - - - - ); - }} - + mode="proposal" + prepareProposalData={prepareProposal} + /> ); } diff --git a/src/types/createProposal.ts b/src/types/createProposal.ts deleted file mode 100644 index dbb8522d44..0000000000 --- a/src/types/createProposal.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { BigNumberValuePair } from './common'; - -export enum CreateProposalState { - METADATA_FORM, - TRANSACTIONS_FORM, -} - -export interface CreateProposalTransaction { - targetAddress: string; - ethValue: T; - functionName: string; - functionSignature: string; - parameters: string; - encodedFunctionData?: string; -} - -export type ProposalMetadata = { - title: string; - description: string; - documentationUrl: string; -}; - -export type CreateProposalForm = { - transactions: CreateProposalTransaction[]; - proposalMetadata: ProposalMetadata; - nonce?: number; -}; diff --git a/src/types/daoProposal.ts b/src/types/daoProposal.ts index 3783e86e4f..9e4f57de49 100644 --- a/src/types/daoProposal.ts +++ b/src/types/daoProposal.ts @@ -1,11 +1,11 @@ import { BigNumber, BigNumberish } from 'ethers'; -import { ProposalMetadata } from './createProposal'; import { GovernanceActivity } from './fractal'; +import { CreateProposalMetadata } from './proposalBuilder'; import { SafeMultisigConfirmationResponse } from './safeGlobal'; import { MetaTransaction, DecodedTransaction } from './transaction'; export interface ProposalExecuteData extends ExecuteData { - metaData: ProposalMetadata; + metaData: CreateProposalMetadata; } export interface ExecuteData { @@ -20,7 +20,7 @@ export type CreateProposalFunc = (proposal: { }) => void; export type ProposalData = { - metaData?: ProposalMetadata; + metaData?: CreateProposalMetadata; transactions?: MetaTransaction[]; decodedTransactions: DecodedTransaction[]; }; diff --git a/src/types/index.ts b/src/types/index.ts index e4968af8de..39819b0953 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -2,7 +2,7 @@ export * from './account'; export * from './common'; export * from './contract'; export * from './createDAO'; -export * from './createProposal'; +export * from './proposalBuilder'; export * from './daoGeneral'; export * from './daoGovernance'; export * from './daoGuard'; diff --git a/src/types/proposalBuilder.ts b/src/types/proposalBuilder.ts index 8b957137e9..28dbf9859b 100644 --- a/src/types/proposalBuilder.ts +++ b/src/types/proposalBuilder.ts @@ -1,5 +1,9 @@ import { BigNumberValuePair } from './common'; +export enum CreateProposalState { + METADATA_FORM, + TRANSACTIONS_FORM, +} export interface CreateProposalTransaction { targetAddress: string; ethValue: T; @@ -14,8 +18,10 @@ export interface CreateProposalTransaction { export type CreateProposalMetadata = { title: string; description: string; + documentationUrl?: string; }; +export type ProposalBuilderMode = 'proposal' | 'template'; export type CreateProposalForm = { transactions: CreateProposalTransaction[]; proposalMetadata: CreateProposalMetadata; From 6b0aade9272e369fd44c407ec8366b005c7b5b74 Mon Sep 17 00:00:00 2001 From: Kirill Klimenko Date: Mon, 8 Apr 2024 20:31:41 +0200 Subject: [PATCH 14/55] Fix problem with missing bigNumberValue during prepareProposal --- src/hooks/DAO/proposal/usePrepareProposal.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/hooks/DAO/proposal/usePrepareProposal.ts b/src/hooks/DAO/proposal/usePrepareProposal.ts index c9914cd0b3..54928a6360 100644 --- a/src/hooks/DAO/proposal/usePrepareProposal.ts +++ b/src/hooks/DAO/proposal/usePrepareProposal.ts @@ -25,7 +25,9 @@ export function usePrepareProposal() { ); return { targets, - values: transactionsWithEncoding.map(transaction => transaction.ethValue.bigNumberValue!), + values: transactionsWithEncoding.map( + transaction => transaction.ethValue.bigNumberValue || 0, + ), calldatas: transactionsWithEncoding.map( transaction => transaction.encodedFunctionData || '', ), From ac192ff959efc52820a75c0c22d8a4ed25bd5cfb Mon Sep 17 00:00:00 2001 From: Kirill Klimenko Date: Mon, 8 Apr 2024 20:56:38 +0200 Subject: [PATCH 15/55] Explicitly set bigNumberValue to null while creating proposal template and within BigNumberInput --- src/components/CreateProposalTemplate/constants.ts | 2 +- .../DaoCreator/formComponents/AzoriusTokenAllocation.tsx | 2 +- src/components/ui/forms/BigNumberInput.tsx | 6 +++--- src/hooks/utils/useApproval.tsx | 2 +- src/types/common.ts | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/CreateProposalTemplate/constants.ts b/src/components/CreateProposalTemplate/constants.ts index 4c7930e184..d7673dd6fd 100644 --- a/src/components/CreateProposalTemplate/constants.ts +++ b/src/components/CreateProposalTemplate/constants.ts @@ -2,7 +2,7 @@ import { CreateProposalTemplateTransaction } from '../../types/createProposalTem export const DEFAULT_PROPOSAL_TEMPLATE_TRANSACTION: CreateProposalTemplateTransaction = { targetAddress: '', - ethValue: { value: '', bigNumberValue: undefined }, + ethValue: { value: '', bigNumberValue: null }, functionName: '', parameters: [ { diff --git a/src/components/DaoCreator/formComponents/AzoriusTokenAllocation.tsx b/src/components/DaoCreator/formComponents/AzoriusTokenAllocation.tsx index c066bdda74..4c85f26ce0 100644 --- a/src/components/DaoCreator/formComponents/AzoriusTokenAllocation.tsx +++ b/src/components/DaoCreator/formComponents/AzoriusTokenAllocation.tsx @@ -11,7 +11,7 @@ interface ITokenAllocations { setFieldValue: (field: string, value: any) => void; addressErrorMessage: string | null; amountErrorMessage: string | null; - amountInputValue: BigNumber | undefined; + amountInputValue: BigNumber | undefined | null; allocationLength: number; } diff --git a/src/components/ui/forms/BigNumberInput.tsx b/src/components/ui/forms/BigNumberInput.tsx index 02214580b5..51cf769f89 100644 --- a/src/components/ui/forms/BigNumberInput.tsx +++ b/src/components/ui/forms/BigNumberInput.tsx @@ -13,12 +13,12 @@ import { BigNumberValuePair } from '../../../types'; export interface BigNumberInputProps extends Omit, FormControlOptions { - value: BigNumber | undefined; + value: BigNumber | undefined | null; onChange: (value: BigNumberValuePair) => void; decimalPlaces?: number; min?: string; max?: string; - maxValue?: BigNumber; + maxValue?: BigNumber | null; } /** * This component will add a chakra Input component that accepts and sets a BigNumber @@ -82,7 +82,7 @@ export function BigNumberInput({ if (stringValue === '') { onChange({ value: stringValue, - bigNumberValue: undefined, + bigNumberValue: null, }); setInputValue(''); return; diff --git a/src/hooks/utils/useApproval.tsx b/src/hooks/utils/useApproval.tsx index 681a41a0c6..8e8881198b 100644 --- a/src/hooks/utils/useApproval.tsx +++ b/src/hooks/utils/useApproval.tsx @@ -8,7 +8,7 @@ import { useTransaction } from './useTransaction'; const useApproval = ( tokenContract?: VotesERC20 | VotesERC20Wrapper, spenderAddress?: string, - userBalance?: BigNumber, + userBalance?: BigNumber | null, ) => { const { address: account } = useAccount(); const [allowance, setAllowance] = useState(BigNumber.from(0)); diff --git a/src/types/common.ts b/src/types/common.ts index de450d9095..e611af1d7b 100644 --- a/src/types/common.ts +++ b/src/types/common.ts @@ -2,7 +2,7 @@ import { BigNumber } from 'ethers'; export interface BigNumberValuePair { value: string; - bigNumberValue?: BigNumber; + bigNumberValue?: BigNumber | null; } export type WithError = { error?: string }; From e3ff66bbc0d7130ab71271eb67257371a21a03be Mon Sep 17 00:00:00 2001 From: Kirill Klimenko Date: Tue, 9 Apr 2024 12:44:34 +0200 Subject: [PATCH 16/55] Do not type amountInputValue and value on BigNumberInput with explicit undefined --- .../DaoCreator/formComponents/AzoriusTokenAllocation.tsx | 2 +- src/components/ui/forms/BigNumberInput.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/DaoCreator/formComponents/AzoriusTokenAllocation.tsx b/src/components/DaoCreator/formComponents/AzoriusTokenAllocation.tsx index 4c85f26ce0..4f6f574d50 100644 --- a/src/components/DaoCreator/formComponents/AzoriusTokenAllocation.tsx +++ b/src/components/DaoCreator/formComponents/AzoriusTokenAllocation.tsx @@ -11,7 +11,7 @@ interface ITokenAllocations { setFieldValue: (field: string, value: any) => void; addressErrorMessage: string | null; amountErrorMessage: string | null; - amountInputValue: BigNumber | undefined | null; + amountInputValue?: BigNumber | null; allocationLength: number; } diff --git a/src/components/ui/forms/BigNumberInput.tsx b/src/components/ui/forms/BigNumberInput.tsx index 51cf769f89..c7a8229ed2 100644 --- a/src/components/ui/forms/BigNumberInput.tsx +++ b/src/components/ui/forms/BigNumberInput.tsx @@ -13,7 +13,7 @@ import { BigNumberValuePair } from '../../../types'; export interface BigNumberInputProps extends Omit, FormControlOptions { - value: BigNumber | undefined | null; + value?: BigNumber | null; onChange: (value: BigNumberValuePair) => void; decimalPlaces?: number; min?: string; From 520fe1dbe3b9ff1f9b5a42731d66defa168fe9c2 Mon Sep 17 00:00:00 2001 From: Kirill Klimenko Date: Tue, 9 Apr 2024 12:52:15 +0200 Subject: [PATCH 17/55] Transform ProposalBuilderMode into enum --- src/components/ProposalBuilder/ProposalDetails.tsx | 2 +- src/components/ProposalBuilder/ProposalMetadata.tsx | 2 +- src/components/ProposalBuilder/ProposalTransaction.tsx | 2 +- src/components/ProposalBuilder/index.tsx | 2 +- src/pages/daos/[daoAddress]/proposal-templates/new/index.tsx | 4 ++-- src/pages/daos/[daoAddress]/proposals/new/index.tsx | 5 +++-- src/types/proposalBuilder.ts | 5 ++++- 7 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/components/ProposalBuilder/ProposalDetails.tsx b/src/components/ProposalBuilder/ProposalDetails.tsx index e7b917e2bd..1417d4f830 100644 --- a/src/components/ProposalBuilder/ProposalDetails.tsx +++ b/src/components/ProposalBuilder/ProposalDetails.tsx @@ -52,7 +52,7 @@ export default function ProposalTemplateDetails({ {t('previewTitle')} {trimmedTitle} - {mode === 'template' && ( + {mode === ProposalBuilderMode.TEMPLATE && ( {t('previewThumnbail')} {trimmedTitle && ( diff --git a/src/components/ProposalBuilder/ProposalMetadata.tsx b/src/components/ProposalBuilder/ProposalMetadata.tsx index 556c923027..65bc1b51ff 100644 --- a/src/components/ProposalBuilder/ProposalMetadata.tsx +++ b/src/components/ProposalBuilder/ProposalMetadata.tsx @@ -18,7 +18,7 @@ export default function ProposalMetadata({ mode, }: ProposalMetadataProps) { const { t } = useTranslation(['proposalTemplate', 'proposal', 'common']); - const isProposalMode = mode === 'proposal'; + const isProposalMode = mode === ProposalBuilderMode.PROPOSAL; return ( <> diff --git a/src/components/ProposalBuilder/ProposalTransaction.tsx b/src/components/ProposalBuilder/ProposalTransaction.tsx index ad256b811b..a4876fa195 100644 --- a/src/components/ProposalBuilder/ProposalTransaction.tsx +++ b/src/components/ProposalBuilder/ProposalTransaction.tsx @@ -27,7 +27,7 @@ export default function ProposalTransaction({ setFieldValue, mode, }: ProposalTransactionProps) { - const isProposalMode = mode === 'proposal'; + const isProposalMode = mode === ProposalBuilderMode.PROPOSAL; const { t } = useTranslation(['proposal', 'proposalTemplate', 'common']); const handleABISelectorChange = useCallback( (value: ABIElement) => { diff --git a/src/components/ProposalBuilder/index.tsx b/src/components/ProposalBuilder/index.tsx index 93d3cbfe19..57e2fc3038 100644 --- a/src/components/ProposalBuilder/index.tsx +++ b/src/components/ProposalBuilder/index.tsx @@ -37,7 +37,7 @@ export default function ProposalBuilder({ const [formState, setFormState] = useState(CreateProposalState.METADATA_FORM); const { t } = useTranslation(['proposalTemplate', 'proposal']); - const isProposalMode = mode === 'proposal'; + const isProposalMode = mode === ProposalBuilderMode.PROPOSAL; const navigate = useNavigate(); const { diff --git a/src/pages/daos/[daoAddress]/proposal-templates/new/index.tsx b/src/pages/daos/[daoAddress]/proposal-templates/new/index.tsx index 4ccb364cc3..92a8c53e8e 100644 --- a/src/pages/daos/[daoAddress]/proposal-templates/new/index.tsx +++ b/src/pages/daos/[daoAddress]/proposal-templates/new/index.tsx @@ -6,7 +6,7 @@ import { DEFAULT_PROPOSAL } from '../../../../../components/ProposalBuilder/cons import { logError } from '../../../../../helpers/errorLogging'; import useCreateProposalTemplate from '../../../../../hooks/DAO/proposal/useCreateProposalTemplate'; import useIPFSClient from '../../../../../providers/App/hooks/useIPFSClient'; -import { ProposalTemplate } from '../../../../../types/proposalBuilder'; +import { ProposalBuilderMode, ProposalTemplate } from '../../../../../types/proposalBuilder'; export default function CreateProposalTemplatePage() { const ipfsClient = useIPFSClient(); @@ -55,7 +55,7 @@ export default function CreateProposalTemplatePage() { return ( diff --git a/src/pages/daos/[daoAddress]/proposals/new/index.tsx b/src/pages/daos/[daoAddress]/proposals/new/index.tsx index 7a33cb0a6d..fada2ade2c 100644 --- a/src/pages/daos/[daoAddress]/proposals/new/index.tsx +++ b/src/pages/daos/[daoAddress]/proposals/new/index.tsx @@ -5,8 +5,9 @@ import { BarLoader } from '../../../../../components/ui/loaders/BarLoader'; import { HEADER_HEIGHT } from '../../../../../constants/common'; import { usePrepareProposal } from '../../../../../hooks/DAO/proposal/usePrepareProposal'; import { useFractal } from '../../../../../providers/App/AppProvider'; +import { ProposalBuilderMode } from '../../../../../types'; -export default function ProposalCreatePage() { +export default function CreateProposalPage() { const { node: { daoAddress, safe }, governance: { type }, @@ -24,7 +25,7 @@ export default function ProposalCreatePage() { return ( ); diff --git a/src/types/proposalBuilder.ts b/src/types/proposalBuilder.ts index 28dbf9859b..e3e59d207e 100644 --- a/src/types/proposalBuilder.ts +++ b/src/types/proposalBuilder.ts @@ -21,7 +21,10 @@ export type CreateProposalMetadata = { documentationUrl?: string; }; -export type ProposalBuilderMode = 'proposal' | 'template'; +export enum ProposalBuilderMode { + PROPOSAL = 'PROPOSAL', + TEMPLATE = 'TEMPLATE' +}; export type CreateProposalForm = { transactions: CreateProposalTransaction[]; proposalMetadata: CreateProposalMetadata; From ee0d4fd8f3c096e8cc6f3e868f3e6b1547849dcc Mon Sep 17 00:00:00 2001 From: Kirill Klimenko Date: Tue, 9 Apr 2024 12:53:41 +0200 Subject: [PATCH 18/55] Prettier --- src/types/proposalBuilder.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/types/proposalBuilder.ts b/src/types/proposalBuilder.ts index e3e59d207e..79a3db8345 100644 --- a/src/types/proposalBuilder.ts +++ b/src/types/proposalBuilder.ts @@ -23,8 +23,8 @@ export type CreateProposalMetadata = { export enum ProposalBuilderMode { PROPOSAL = 'PROPOSAL', - TEMPLATE = 'TEMPLATE' -}; + TEMPLATE = 'TEMPLATE', +} export type CreateProposalForm = { transactions: CreateProposalTransaction[]; proposalMetadata: CreateProposalMetadata; From 4fa08c6a7f5c0a701d291254a08d6c4179fc9b99 Mon Sep 17 00:00:00 2001 From: Kirill Klimenko Date: Tue, 9 Apr 2024 13:21:19 +0200 Subject: [PATCH 19/55] If signature is empty - do not call encodeFunction but simply return 0x as calldata --- src/hooks/DAO/proposal/usePrepareProposal.ts | 26 +++++++++++--------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/src/hooks/DAO/proposal/usePrepareProposal.ts b/src/hooks/DAO/proposal/usePrepareProposal.ts index e81a71463c..945150104b 100644 --- a/src/hooks/DAO/proposal/usePrepareProposal.ts +++ b/src/hooks/DAO/proposal/usePrepareProposal.ts @@ -10,19 +10,22 @@ export function usePrepareProposal() { async (values: CreateProposalForm) => { const { transactions, proposalMetadata } = values; const transactionsWithEncoding = transactions.map(tx => { + const signature = tx.parameters.map(parameter => parameter.signature.trim()).join(', '); + const parameters = tx.parameters + .map(parameter => + isValidUrl(parameter.value!.trim()) + ? encodeURIComponent(parameter.value!.trim()) // If parameter.value is valid URL with special symbols like ":" or "?" - decoding might fail, thus we need to encode URL + : parameter.value!.trim(), + ) + .join(', ') + return { ...tx, - encodedFunctionData: encodeFunction( + calldata: signature ? encodeFunction( tx.functionName, - tx.parameters.map(parameter => parameter.signature.trim()).join(', '), - tx.parameters - .map(parameter => - isValidUrl(parameter.value!.trim()) - ? encodeURIComponent(parameter.value!.trim()) // If parameter.value is valid URL with special symbols like ":" or "?" - decoding might fail, thus we need to encode URL - : parameter.value!.trim(), - ) - .join(', '), - ), + signature, + parameters, + ) : '0x', }; }); const targets = await Promise.all( @@ -33,11 +36,12 @@ export function usePrepareProposal() { return tx.targetAddress; }), ); + return { targets, values: transactionsWithEncoding.map(transaction => transaction.ethValue.bigNumberValue!), calldatas: transactionsWithEncoding.map( - transaction => transaction.encodedFunctionData || '', + transaction => transaction.calldata || '', ), metaData: { title: proposalMetadata.title, From 6886d707be026761079803ba3e705700ab4092f5 Mon Sep 17 00:00:00 2001 From: Kirill Klimenko Date: Tue, 9 Apr 2024 13:26:18 +0200 Subject: [PATCH 20/55] Validate based on function name and not signature. If function name not specified - immediately return transaction with calldata set to 0x --- src/hooks/DAO/proposal/usePrepareProposal.ts | 39 ++++++++++---------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/src/hooks/DAO/proposal/usePrepareProposal.ts b/src/hooks/DAO/proposal/usePrepareProposal.ts index 945150104b..d8ca234c08 100644 --- a/src/hooks/DAO/proposal/usePrepareProposal.ts +++ b/src/hooks/DAO/proposal/usePrepareProposal.ts @@ -10,23 +10,26 @@ export function usePrepareProposal() { async (values: CreateProposalForm) => { const { transactions, proposalMetadata } = values; const transactionsWithEncoding = transactions.map(tx => { - const signature = tx.parameters.map(parameter => parameter.signature.trim()).join(', '); - const parameters = tx.parameters - .map(parameter => - isValidUrl(parameter.value!.trim()) - ? encodeURIComponent(parameter.value!.trim()) // If parameter.value is valid URL with special symbols like ":" or "?" - decoding might fail, thus we need to encode URL - : parameter.value!.trim(), - ) - .join(', ') + if (!tx.functionName) { + return { + ...tx, + calldata: '0x', + }; + } else { + const signature = tx.parameters.map(parameter => parameter.signature.trim()).join(', '); + const parameters = tx.parameters + .map(parameter => + isValidUrl(parameter.value!.trim()) + ? encodeURIComponent(parameter.value!.trim()) // If parameter.value is valid URL with special symbols like ":" or "?" - decoding might fail, thus we need to encode URL + : parameter.value!.trim(), + ) + .join(', '); - return { - ...tx, - calldata: signature ? encodeFunction( - tx.functionName, - signature, - parameters, - ) : '0x', - }; + return { + ...tx, + calldata: encodeFunction(tx.functionName, signature, parameters), + }; + } }); const targets = await Promise.all( transactionsWithEncoding.map(tx => { @@ -40,9 +43,7 @@ export function usePrepareProposal() { return { targets, values: transactionsWithEncoding.map(transaction => transaction.ethValue.bigNumberValue!), - calldatas: transactionsWithEncoding.map( - transaction => transaction.calldata || '', - ), + calldatas: transactionsWithEncoding.map(transaction => transaction.calldata || ''), metaData: { title: proposalMetadata.title, description: proposalMetadata.description, From b26a222d0eea43035f9b34dd6837a442f882a2f1 Mon Sep 17 00:00:00 2001 From: Kirill Klimenko Date: Tue, 9 Apr 2024 17:44:04 +0200 Subject: [PATCH 21/55] Rearrange order of types in useGetMultisigMetadata --- src/hooks/DAO/proposal/useGetMetadata.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/DAO/proposal/useGetMetadata.ts b/src/hooks/DAO/proposal/useGetMetadata.ts index 6a80aeee06..cf7bf93d7a 100644 --- a/src/hooks/DAO/proposal/useGetMetadata.ts +++ b/src/hooks/DAO/proposal/useGetMetadata.ts @@ -25,7 +25,7 @@ interface Transaction { const useGetMultisigMetadata = (proposal: FractalProposal | null | undefined) => { const ipfsClient = useIPFSClient(); const [multisigMetadata, setMultisigMetadata] = useState< - undefined | CreateProposalMetadata | null + CreateProposalMetadata | null | undefined >(undefined); const [setValue, getValue] = useIndexedDB(DBObjectKeys.DECODED_TRANSACTIONS); From 95f769b6e01b6accf1201a2f5408f306b35a405b Mon Sep 17 00:00:00 2001 From: Adam Gall Date: Wed, 10 Apr 2024 16:25:08 -0400 Subject: [PATCH 22/55] Create helper in useSafeAPI to get Safe Info + Next Nonce --- src/providers/App/hooks/useSafeAPI.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/providers/App/hooks/useSafeAPI.ts b/src/providers/App/hooks/useSafeAPI.ts index 6ad8193932..2153ecd159 100644 --- a/src/providers/App/hooks/useSafeAPI.ts +++ b/src/providers/App/hooks/useSafeAPI.ts @@ -223,13 +223,20 @@ class CachingSafeServiceClient extends SafeServiceClient { }); return value; } + + async getSafeData(safeAddress: string): Promise { + const safeInfoResponse = await this.getSafeInfo(safeAddress); + const nextNonce = await this.getNextNonce(safeAddress); + const safeInfo = { ...safeInfoResponse, nonce: nextNonce }; + return safeInfo; + } } export function useSafeAPI() { const { safeBaseURL, chainId } = useNetworkConfig(); const signerOrProvider = useSignerOrProvider(); - const safeAPI: SafeServiceClient | undefined = useMemo(() => { + const safeAPI = useMemo(() => { if (!signerOrProvider) { return undefined; } From 418892d4e0b014e4a2a00deeb5bf6775ab84be61 Mon Sep 17 00:00:00 2001 From: Adam Gall Date: Wed, 10 Apr 2024 16:25:20 -0400 Subject: [PATCH 23/55] Use that helper in all the places --- src/hooks/DAO/loaders/useFractalNode.ts | 5 +---- src/hooks/DAO/loaders/useLoadDAONode.ts | 6 ++---- src/hooks/DAO/useClawBack.ts | 5 ++--- 3 files changed, 5 insertions(+), 11 deletions(-) diff --git a/src/hooks/DAO/loaders/useFractalNode.ts b/src/hooks/DAO/loaders/useFractalNode.ts index 9337b0006a..3d41116001 100644 --- a/src/hooks/DAO/loaders/useFractalNode.ts +++ b/src/hooks/DAO/loaders/useFractalNode.ts @@ -100,11 +100,8 @@ export const useFractalNode = ( try { if (!safeAPI) throw new Error('SafeAPI not set'); - const address = utils.getAddress(_daoAddress); - const safeInfoResponse = await safeAPI.getSafeInfo(address); - const nextNonce = await safeAPI.getNextNonce(address); - safeInfo = { ...safeInfoResponse, nonce: nextNonce }; + safeInfo = await safeAPI.getSafeData(address); } catch (e) { reset({ error: true }); return; diff --git a/src/hooks/DAO/loaders/useLoadDAONode.ts b/src/hooks/DAO/loaders/useLoadDAONode.ts index a7840fc786..7cf0fe8f63 100644 --- a/src/hooks/DAO/loaders/useLoadDAONode.ts +++ b/src/hooks/DAO/loaders/useLoadDAONode.ts @@ -56,14 +56,12 @@ export const useLoadDAONode = () => { } const sanitizedDaoAddress = utils.getAddress(_daoAddress); - const safeInfo = await safeAPI.getSafeInfo(sanitizedDaoAddress); - const nextNonce = await safeAPI.getNextNonce(sanitizedDaoAddress); - const safeInfoWithGuard = { ...safeInfo, nonce: nextNonce }; + const safeInfoWithGuard = await safeAPI.getSafeData(sanitizedDaoAddress); const node: FractalNode = Object.assign(graphNodeInfo, { daoName: await getDaoName(sanitizedDaoAddress, graphNodeInfo.daoName), safe: safeInfoWithGuard, - fractalModules: await lookupModules(safeInfo.modules), + fractalModules: await lookupModules(safeInfoWithGuard.modules), }); // TODO we could cache node here, but should be careful not to cache diff --git a/src/hooks/DAO/useClawBack.ts b/src/hooks/DAO/useClawBack.ts index 8b685623ab..8ac284eda8 100644 --- a/src/hooks/DAO/useClawBack.ts +++ b/src/hooks/DAO/useClawBack.ts @@ -27,8 +27,7 @@ export default function useClawBack({ childSafeInfo, parentAddress }: IUseClawBa ); const santitizedParentAddress = utils.getAddress(parentAddress); - const parentSafeInfo = await safeAPI.getSafeInfo(santitizedParentAddress); - const parentSafeNextNonce = await safeAPI.getNextNonce(santitizedParentAddress); + const parentSafeInfo = await safeAPI.getSafeData(santitizedParentAddress); if (canUserCreateProposal && parentAddress && childSafeInfo && parentSafeInfo) { const abiCoder = new ethers.utils.AbiCoder(); @@ -92,7 +91,7 @@ export default function useClawBack({ childSafeInfo, parentAddress }: IUseClawBa values: transactions.map(tx => tx.value), calldatas: transactions.map(tx => tx.calldata), }, - nonce: parentSafeNextNonce, + nonce: parentSafeInfo.nonce, pendingToastMessage: t('clawBackPendingToastMessage'), failedToastMessage: t('clawBackFailedToastMessage'), successToastMessage: t('clawBackSuccessToastMessage'), From b5247fc9b1a8d3108c6fbcb0eec8f06517f1c30b Mon Sep 17 00:00:00 2001 From: Adam Gall Date: Wed, 10 Apr 2024 16:26:42 -0400 Subject: [PATCH 24/55] When loading New Proposal page, re-fetch Safe Info --- .../daos/[daoAddress]/proposals/new/index.tsx | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/pages/daos/[daoAddress]/proposals/new/index.tsx b/src/pages/daos/[daoAddress]/proposals/new/index.tsx index bd0954259a..92d2e10499 100644 --- a/src/pages/daos/[daoAddress]/proposals/new/index.tsx +++ b/src/pages/daos/[daoAddress]/proposals/new/index.tsx @@ -1,7 +1,7 @@ import { Grid, GridItem, Box, Flex, Center } from '@chakra-ui/react'; import { Trash } from '@decent-org/fractal-ui'; import { Formik, FormikProps } from 'formik'; -import { useState, useMemo } from 'react'; +import { useState, useMemo, useEffect, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; import { ProposalDetails } from '../../../../../components/ProposalCreate/ProposalDetails'; @@ -18,6 +18,8 @@ import useSubmitProposal from '../../../../../hooks/DAO/proposal/useSubmitPropos import { useCreateProposalSchema } from '../../../../../hooks/schemas/proposalCreate/useCreateProposalSchema'; import { useCanUserCreateProposal } from '../../../../../hooks/utils/useCanUserSubmitProposal'; import { useFractal } from '../../../../../providers/App/AppProvider'; +import { useSafeAPI } from '../../../../../providers/App/hooks/useSafeAPI'; +import { NodeAction } from '../../../../../providers/App/node/action'; import { useNetworkConfig } from '../../../../../providers/NetworkConfig/NetworkConfigProvider'; import { CreateProposalForm, CreateProposalState, GovernanceType } from '../../../../../types'; @@ -27,9 +29,11 @@ const templateAreaSingleCol = `"content" export default function ProposalCreatePage() { const { + action, node: { daoAddress, safe }, governance: { type }, } = useFractal(); + const safeAPI = useSafeAPI(); const { addressPrefix } = useNetworkConfig(); const { createProposalValidation } = useCreateProposalSchema(); const { prepareProposal } = usePrepareProposal(); @@ -51,6 +55,23 @@ export default function ProposalCreatePage() { } }; + const isMounted = useRef(false); + useEffect(() => { + if (!safeAPI || !daoAddress || isMounted.current) { + return; + } + + (async () => { + const safeInfo = await safeAPI.getSafeData(daoAddress); + action.dispatch({ + type: NodeAction.SET_SAFE_INFO, + payload: safeInfo, + }); + })(); + + isMounted.current = true; + }, [action, daoAddress, safeAPI]); + if (!type || !daoAddress || !safe) { return (
From 773bb5eb0b1ca7e61908cec9fb3e82c6b85aef46 Mon Sep 17 00:00:00 2001 From: Adam Gall Date: Wed, 10 Apr 2024 16:27:12 -0400 Subject: [PATCH 25/55] Allow Formik to reinitialize it's initial values, to account for potentially updated nonce --- src/pages/daos/[daoAddress]/proposals/new/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/daos/[daoAddress]/proposals/new/index.tsx b/src/pages/daos/[daoAddress]/proposals/new/index.tsx index 92d2e10499..b4afe8b0ac 100644 --- a/src/pages/daos/[daoAddress]/proposals/new/index.tsx +++ b/src/pages/daos/[daoAddress]/proposals/new/index.tsx @@ -84,6 +84,7 @@ export default function ProposalCreatePage() { validationSchema={createProposalValidation} initialValues={{ ...DEFAULT_PROPOSAL, nonce: safe.nonce }} + enableReinitialize onSubmit={async values => { const { nonce } = values; const proposalData = await prepareProposal(values); From 49159e9de0cd37c9d29664a44498b88d24c9ada6 Mon Sep 17 00:00:00 2001 From: Kellar Date: Thu, 11 Apr 2024 14:19:42 +0100 Subject: [PATCH 26/55] Remove max length on snapshotURL input --- src/components/DaoCreator/formComponents/EstablishEssentials.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/DaoCreator/formComponents/EstablishEssentials.tsx b/src/components/DaoCreator/formComponents/EstablishEssentials.tsx index 89de33253d..7242ecbf20 100644 --- a/src/components/DaoCreator/formComponents/EstablishEssentials.tsx +++ b/src/components/DaoCreator/formComponents/EstablishEssentials.tsx @@ -167,7 +167,6 @@ export function EstablishEssentials(props: ICreationStepProps) { isDisabled={snapshotURLDisabled} data-testid="essentials-snapshotURL" placeholder="example.eth" - maxLength={30} /> From 278dcf90c60f1462e64706562bcfd6e5a92dd1f4 Mon Sep 17 00:00:00 2001 From: Kyrylo Klymenko Date: Sun, 14 Apr 2024 17:01:07 +0300 Subject: [PATCH 27/55] Update src/i18n/locales/en/proposal.json Update helperTargetAddress copy Co-authored-by: Adam Gall --- src/i18n/locales/en/proposal.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/locales/en/proposal.json b/src/i18n/locales/en/proposal.json index c01bebca56..fe1656b144 100644 --- a/src/i18n/locales/en/proposal.json +++ b/src/i18n/locales/en/proposal.json @@ -2,7 +2,7 @@ "proposalOverview": "Proposal Overview", "breakdownTitle": "Breakdown", "labelTargetAddress": "Target Address", - "helperTargetAddress": "The smart contract address this proposal will modify. Paste address and we'll try to fetch ABI for this contract from Etherscan", + "helperTargetAddress": "The smart contract address this proposal will modify. Paste an address and we'll try to fetch the ABI for this contract.", "labelFunctionName": "Function Name", "helperFunctionName": "The name of the function to be called if this proposal passes", "labelFunctionSignature": "Function Signature", From db9e40942bb7f52c54b515a31351e2ed22033e26 Mon Sep 17 00:00:00 2001 From: Kirill Klimenko Date: Sun, 14 Apr 2024 16:18:09 +0200 Subject: [PATCH 28/55] Fix import --- .../DaoCreator/formComponents/AzoriusTokenAllocation.tsx | 2 +- src/components/ui/forms/BigIntInput.tsx | 2 +- src/types/daoProposal.ts | 1 - src/types/fractal.ts | 4 ++-- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/components/DaoCreator/formComponents/AzoriusTokenAllocation.tsx b/src/components/DaoCreator/formComponents/AzoriusTokenAllocation.tsx index 67d484c7d3..bd73002b2a 100644 --- a/src/components/DaoCreator/formComponents/AzoriusTokenAllocation.tsx +++ b/src/components/DaoCreator/formComponents/AzoriusTokenAllocation.tsx @@ -10,7 +10,7 @@ interface ITokenAllocations { setFieldValue: (field: string, value: any) => void; addressErrorMessage: string | null; amountErrorMessage: string | null; - amountInputValue: bigint | undefined; + amountInputValue?: bigint; allocationLength: number; } diff --git a/src/components/ui/forms/BigIntInput.tsx b/src/components/ui/forms/BigIntInput.tsx index e1a43bf0e5..58ef224efd 100644 --- a/src/components/ui/forms/BigIntInput.tsx +++ b/src/components/ui/forms/BigIntInput.tsx @@ -13,7 +13,7 @@ import { BigIntValuePair } from '../../../types'; export interface BigIntInputProps extends Omit, FormControlOptions { - value: bigint | undefined; + value?: bigint; onChange: (value: BigIntValuePair) => void; decimalPlaces?: number; min?: string; diff --git a/src/types/daoProposal.ts b/src/types/daoProposal.ts index 9cbd02d171..cc6bf6712b 100644 --- a/src/types/daoProposal.ts +++ b/src/types/daoProposal.ts @@ -1,4 +1,3 @@ -import { ProposalMetadata } from './createProposal'; import { GovernanceActivity } from './fractal'; import { CreateProposalMetadata } from './proposalBuilder'; import { SafeMultisigConfirmationResponse } from './safeGlobal'; diff --git a/src/types/fractal.ts b/src/types/fractal.ts index 92cc8eef73..8c0bc7b3b6 100644 --- a/src/types/fractal.ts +++ b/src/types/fractal.ts @@ -177,10 +177,10 @@ export enum SafeTransferType { } export interface ITokenAccount { - userBalance: bigint | undefined; + userBalance?: bigint; userBalanceString: string | undefined; delegatee: string | undefined; - votingWeight: bigint | undefined; + votingWeight?: bigint; votingWeightString: string | undefined; isDelegatesSet: boolean | undefined; } From b9514d94d433828a5bb35feb21580f3d307d8f61 Mon Sep 17 00:00:00 2001 From: Kirill Klimenko Date: Sun, 14 Apr 2024 16:28:53 +0200 Subject: [PATCH 29/55] Prettier --- src/hooks/DAO/proposal/usePrepareProposal.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/hooks/DAO/proposal/usePrepareProposal.ts b/src/hooks/DAO/proposal/usePrepareProposal.ts index 2e145de92f..4e60043391 100644 --- a/src/hooks/DAO/proposal/usePrepareProposal.ts +++ b/src/hooks/DAO/proposal/usePrepareProposal.ts @@ -42,9 +42,7 @@ export function usePrepareProposal() { return { targets, - values: transactionsWithEncoding.map( - transaction => transaction.ethValue.bigintValue || 0n, - ), + values: transactionsWithEncoding.map(transaction => transaction.ethValue.bigintValue || 0n), calldatas: transactionsWithEncoding.map(transaction => transaction.calldata || ''), metaData: { title: proposalMetadata.title, From 5375892c75f43f9470d7bc8cfa93235b71131a04 Mon Sep 17 00:00:00 2001 From: Kellar Date: Mon, 15 Apr 2024 19:49:42 +0100 Subject: [PATCH 30/55] Have snapshot spaces only load from prod env --- src/hooks/DAO/loaders/snapshot/index.ts | 5 ++--- src/hooks/DAO/loaders/snapshot/useSnapshotProposal.ts | 7 +------ src/hooks/DAO/loaders/snapshot/useSnapshotProposals.ts | 7 +------ 3 files changed, 4 insertions(+), 15 deletions(-) diff --git a/src/hooks/DAO/loaders/snapshot/index.ts b/src/hooks/DAO/loaders/snapshot/index.ts index 4c6c14b179..a458f7db38 100644 --- a/src/hooks/DAO/loaders/snapshot/index.ts +++ b/src/hooks/DAO/loaders/snapshot/index.ts @@ -11,9 +11,8 @@ const defaultOptions: DefaultOptions = { }, }; -export const createClient = (uri: string) => - new ApolloClient({ - uri: `https://${uri.includes('testnet') ? 'testnet.' : ''}hub.snapshot.org/graphql`, +export const createClient = () => new ApolloClient({ + uri: 'https://hub.snapshot.org/graphql', cache: new InMemoryCache(), defaultOptions, }); diff --git a/src/hooks/DAO/loaders/snapshot/useSnapshotProposal.ts b/src/hooks/DAO/loaders/snapshot/useSnapshotProposal.ts index 24ea78a238..adb96e3aed 100644 --- a/src/hooks/DAO/loaders/snapshot/useSnapshotProposal.ts +++ b/src/hooks/DAO/loaders/snapshot/useSnapshotProposal.ts @@ -17,17 +17,12 @@ export default function useSnapshotProposal(proposal: FractalProposal | null | u const [extendedSnapshotProposal, setExtendedSnapshotProposal] = useState(); const { - node: { daoSnapshotURL }, readOnly: { user: { address }, }, } = useFractal(); const daoSnapshotSpaceName = useSnapshotSpaceName(); - const client = useMemo(() => { - if (daoSnapshotURL) { - return createClient(daoSnapshotURL); - } - }, [daoSnapshotURL]); + const client = useMemo(() => createClient(), []); const snapshotProposal = proposal as SnapshotProposal; const isSnapshotProposal = useMemo( diff --git a/src/hooks/DAO/loaders/snapshot/useSnapshotProposals.ts b/src/hooks/DAO/loaders/snapshot/useSnapshotProposals.ts index 3156b53459..722f380dd7 100644 --- a/src/hooks/DAO/loaders/snapshot/useSnapshotProposals.ts +++ b/src/hooks/DAO/loaders/snapshot/useSnapshotProposals.ts @@ -9,16 +9,11 @@ import { createClient } from './'; export const useSnapshotProposals = () => { const { - node: { daoSnapshotURL }, action, } = useFractal(); const daoSnapshotSpaceName = useSnapshotSpaceName(); const currentSnapshotURL = useRef(); - const client = useMemo(() => { - if (daoSnapshotURL) { - return createClient(daoSnapshotURL); - } - }, [daoSnapshotURL]); + const client = useMemo(() => createClient(), []); const loadSnapshotProposals = useCallback(async () => { if (client) { From 281283a1a2506071de9c8c1ec4d2416848a31c21 Mon Sep 17 00:00:00 2001 From: Kellar Date: Mon, 15 Apr 2024 20:15:50 +0100 Subject: [PATCH 31/55] Fix broken snapshot link --- src/components/ui/badges/Snapshot.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/ui/badges/Snapshot.tsx b/src/components/ui/badges/Snapshot.tsx index a871ee970c..cb9bc6a928 100644 --- a/src/components/ui/badges/Snapshot.tsx +++ b/src/components/ui/badges/Snapshot.tsx @@ -6,9 +6,10 @@ interface Props extends ButtonProps { } export default function Snapshot({ snapshotURL, mt }: Props) { + const url = snapshotURL.includes('testnet') ? snapshotURL : `https://snapshot.org/#${snapshotURL}`; return ( @@ -193,7 +193,7 @@ export default function MetadataContainer() { )} - {node.daoSnapshotURL && } + {node.daoSnapshotENS && } ); diff --git a/src/hooks/DAO/loaders/useFractalNode.ts b/src/hooks/DAO/loaders/useFractalNode.ts index 6d006ddf1c..e4458612a7 100644 --- a/src/hooks/DAO/loaders/useFractalNode.ts +++ b/src/hooks/DAO/loaders/useFractalNode.ts @@ -49,7 +49,7 @@ export const useFractalNode = ( }, daoName: name as string, daoAddress: utils.getAddress(_daoAddress as string), - daoSnapshotURL: snapshotURL as string, + daoSnapshotENS: snapshotURL as string, proposalTemplatesHash: proposalTemplatesHash as string, }; return currentNode; diff --git a/src/hooks/DAO/loaders/useLoadDAONode.ts b/src/hooks/DAO/loaders/useLoadDAONode.ts index ba5c53d35d..cf4ecaff33 100644 --- a/src/hooks/DAO/loaders/useLoadDAONode.ts +++ b/src/hooks/DAO/loaders/useLoadDAONode.ts @@ -39,7 +39,7 @@ export const useLoadDAONode = () => { }, daoName: name as string, daoAddress: utils.getAddress(_daoAddress as string), - daoSnapshotURL: snapshotURL as string, + daoSnapshotENS: snapshotURL as string, }; return currentNode; } diff --git a/src/hooks/DAO/proposal/useCastVote.ts b/src/hooks/DAO/proposal/useCastVote.ts index f0550e5836..e235da84f6 100644 --- a/src/hooks/DAO/proposal/useCastVote.ts +++ b/src/hooks/DAO/proposal/useCastVote.ts @@ -34,7 +34,7 @@ const useCastVote = ({ const { governanceContracts: { ozLinearVotingContractAddress, erc721LinearVotingContractAddress }, governance, - node: { daoSnapshotURL }, + node: { daoSnapshotENS }, readOnly: { user: { address }, }, @@ -43,15 +43,15 @@ const useCastVote = ({ const daoSnapshotSpaceName = useSnapshotSpaceName(); const signer = useEthersSigner(); const client = useMemo(() => { - if (daoSnapshotURL) { - const isTestnetSnapshotURL = daoSnapshotURL.includes('testnet'); + if (daoSnapshotENS) { + const isTestnetSnapshotURL = daoSnapshotENS.includes('testnet'); const hub = isTestnetSnapshotURL ? 'https://testnet.seq.snapshot.org' // This is not covered in Snapshot docs, but that's where they're sending request on testnet : 'https://hub.snapshot.org'; return new snapshot.Client712(hub); } return undefined; - }, [daoSnapshotURL]); + }, [daoSnapshotENS]); const azoriusGovernance = useMemo(() => governance as AzoriusGovernance, [governance]); const { type } = azoriusGovernance; diff --git a/src/models/DaoTxBuilder.ts b/src/models/DaoTxBuilder.ts index ce6afa7069..1b64e2d568 100644 --- a/src/models/DaoTxBuilder.ts +++ b/src/models/DaoTxBuilder.ts @@ -85,7 +85,7 @@ export class DaoTxBuilder extends BaseTxBuilder { } if (shouldSetSnapshot) { - this.internalTxs = this.internalTxs.concat(this.buildUpdateDAOSnapshotURLTx()); + this.internalTxs = this.internalTxs.concat(this.buildUpdateDAOSnapshotENSTx()); } this.internalTxs = this.internalTxs.concat( @@ -159,7 +159,7 @@ export class DaoTxBuilder extends BaseTxBuilder { const multisigTxBuilder = this.txBuilderFactory.createMultiSigTxBuilder(); this.internalTxs.push(this.buildUpdateDAONameTx()); - this.internalTxs.push(this.buildUpdateDAOSnapshotURLTx()); + this.internalTxs.push(this.buildUpdateDAOSnapshotENSTx()); // subDAO case, add freeze guard if (this.parentAddress) { @@ -227,7 +227,7 @@ export class DaoTxBuilder extends BaseTxBuilder { ); } - private buildUpdateDAOSnapshotURLTx(): SafeTransaction { + private buildUpdateDAOSnapshotENSTx(): SafeTransaction { return buildContractCall( this.baseContracts.keyValuePairsContract, 'updateValues', diff --git a/src/pages/daos/[daoAddress]/edit/governance/index.tsx b/src/pages/daos/[daoAddress]/edit/governance/index.tsx index 174fc56ae1..0a1a7475ce 100644 --- a/src/pages/daos/[daoAddress]/edit/governance/index.tsx +++ b/src/pages/daos/[daoAddress]/edit/governance/index.tsx @@ -21,7 +21,7 @@ import { export default function ModifyGovernancePage() { const { - node: { daoAddress, safe, daoName, daoSnapshotURL }, + node: { daoAddress, safe, daoName, daoSnapshotENS }, governance: { type }, readOnly: { user }, } = useFractal(); @@ -36,7 +36,7 @@ export default function ModifyGovernancePage() { deployAzorius( daoData as AzoriusERC20DAO | AzoriusERC721DAO, !daoName || createAccountSubstring(daoAddress!) === daoName, - !daoSnapshotURL && !!daoData.snapshotURL, + !daoSnapshotENS && !!daoData.snapshotURL, ); }; diff --git a/src/types/fractal.ts b/src/types/fractal.ts index 8c0bc7b3b6..0aaf582bb9 100644 --- a/src/types/fractal.ts +++ b/src/types/fractal.ts @@ -238,7 +238,7 @@ export interface FractalNode { nodeHierarchy: NodeHierarchy; isModulesLoaded?: boolean; isHierarchyLoaded?: boolean; - daoSnapshotURL?: string; + daoSnapshotENS?: string; proposalTemplatesHash?: string; } From 30ee5b03bd081bab9e32f4ef7ba76b637e56b1e0 Mon Sep 17 00:00:00 2001 From: Kellar Date: Tue, 16 Apr 2024 17:33:59 +0100 Subject: [PATCH 45/55] Don't attempt to split snapshot name in `useSnapshotSpaceName` --- src/hooks/DAO/loaders/snapshot/useSnapshotSpaceName.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/hooks/DAO/loaders/snapshot/useSnapshotSpaceName.ts b/src/hooks/DAO/loaders/snapshot/useSnapshotSpaceName.ts index 478930a086..d47fa442ef 100644 --- a/src/hooks/DAO/loaders/snapshot/useSnapshotSpaceName.ts +++ b/src/hooks/DAO/loaders/snapshot/useSnapshotSpaceName.ts @@ -2,8 +2,8 @@ import { useFractal } from '../../../../providers/App/AppProvider'; export default function useSnapshotSpaceName() { const { - node: { daoSnapshotURL }, + node: { daoSnapshotENS }, } = useFractal(); - return daoSnapshotURL?.split('/').pop(); + return daoSnapshotENS; } From b150213045113ff903ad0cb19bba37327d6f3b51 Mon Sep 17 00:00:00 2001 From: Kellar Date: Tue, 16 Apr 2024 17:35:31 +0100 Subject: [PATCH 46/55] Remove snapshot testnet checks reference in `useCastVote.ts`. Use prod URL only --- src/hooks/DAO/proposal/useCastVote.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/hooks/DAO/proposal/useCastVote.ts b/src/hooks/DAO/proposal/useCastVote.ts index e235da84f6..9a75f048b2 100644 --- a/src/hooks/DAO/proposal/useCastVote.ts +++ b/src/hooks/DAO/proposal/useCastVote.ts @@ -44,11 +44,7 @@ const useCastVote = ({ const signer = useEthersSigner(); const client = useMemo(() => { if (daoSnapshotENS) { - const isTestnetSnapshotURL = daoSnapshotENS.includes('testnet'); - const hub = isTestnetSnapshotURL - ? 'https://testnet.seq.snapshot.org' // This is not covered in Snapshot docs, but that's where they're sending request on testnet - : 'https://hub.snapshot.org'; - return new snapshot.Client712(hub); + return new snapshot.Client712('https://hub.snapshot.org'); } return undefined; }, [daoSnapshotENS]); From c2820972e2113403dd5b5be665eb244942df3c81 Mon Sep 17 00:00:00 2001 From: Kellar Date: Wed, 17 Apr 2024 12:41:40 +0100 Subject: [PATCH 47/55] Rename remaining references to snapshotURL to snapshotENS; ensure compatibility with outside world's snapshotURL --- src/components/DaoCreator/constants.ts | 2 +- .../formComponents/EstablishEssentials.tsx | 14 +++++++------- src/components/DaoCreator/index.tsx | 12 +++++++++--- src/components/Proposals/ProposalInfo.tsx | 2 +- .../DaoSettings/components/Metadata/index.tsx | 8 ++++---- src/components/ui/badges/Snapshot.tsx | 6 +++--- src/components/ui/cards/DAOInfoCard.tsx | 2 +- .../DAO/loaders/snapshot/useSnapshotProposals.ts | 6 +++--- src/hooks/DAO/loaders/useFractalNode.ts | 10 ++++++++-- src/hooks/DAO/loaders/useLoadDAONode.ts | 4 ++-- .../schemas/DAOCreate/useDAOCreateSchema.ts | 2 +- src/types/createDAO.ts | 16 +++++++++++++--- src/types/fractal.ts | 4 ++-- 13 files changed, 55 insertions(+), 33 deletions(-) diff --git a/src/components/DaoCreator/constants.ts b/src/components/DaoCreator/constants.ts index 196ad440ed..d29a0f36a7 100644 --- a/src/components/DaoCreator/constants.ts +++ b/src/components/DaoCreator/constants.ts @@ -13,7 +13,7 @@ export const initialState: CreatorFormState = { essentials: { daoName: '', governance: GovernanceType.MULTISIG, - snapshotURL: '', + snapshotENS: '', }, erc20Token: { tokenCreationType: TokenCreationType.NEW, diff --git a/src/components/DaoCreator/formComponents/EstablishEssentials.tsx b/src/components/DaoCreator/formComponents/EstablishEssentials.tsx index 0e40182ffa..48e255c8ed 100644 --- a/src/components/DaoCreator/formComponents/EstablishEssentials.tsx +++ b/src/components/DaoCreator/formComponents/EstablishEssentials.tsx @@ -43,7 +43,7 @@ export function EstablishEssentials(props: ICreationStepProps) { setFieldValue('essentials.daoName', daoName, false); if (createAccountSubstring(daoAddress!) !== daoName) { // Pre-fill the snapshot URL form field when editing - setFieldValue('essentials.snapshotURL', daoSnapshotENS || '', false); + setFieldValue('essentials.snapshotENS', daoSnapshotENS || '', false); } } }, [setFieldValue, mode, daoName, daoSnapshotENS, isEdit, daoAddress]); @@ -52,7 +52,7 @@ export function EstablishEssentials(props: ICreationStepProps) { isEdit && !!daoName && !!daoAddress && createAccountSubstring(daoAddress) !== daoName; // If in governance edit mode and snapshot URL is already set, disable the field - const snapshotURLDisabled = isEdit && !!daoSnapshotENS; + const snapshotENSDisabled = isEdit && !!daoSnapshotENS; const handleGovernanceChange = (value: string) => { if (value === GovernanceType.AZORIUS_ERC20) { @@ -65,7 +65,7 @@ export function EstablishEssentials(props: ICreationStepProps) { }; const handleSnapshotSpaceChange = (value: string) => { - setFieldValue('essentials.snapshotURL', value, true); + setFieldValue('essentials.snapshotENS', value, true); try { ens_normalize(value); setSnapshotSpaceValid(true); @@ -179,12 +179,12 @@ export function EstablishEssentials(props: ICreationStepProps) { helper={t('snapshotHelper')} isRequired={false} > - + handleSnapshotSpaceChange(cEvent.target.value)} - isDisabled={snapshotURLDisabled} - data-testid="essentials-snapshotURL" + isDisabled={snapshotENSDisabled} + data-testid="essentials-snapshotENS" placeholder="example.eth" /> diff --git a/src/components/DaoCreator/index.tsx b/src/components/DaoCreator/index.tsx index e4c3ce5980..e49f575cf7 100644 --- a/src/components/DaoCreator/index.tsx +++ b/src/components/DaoCreator/index.tsx @@ -29,10 +29,16 @@ function DaoCreator({ onSubmit={async values => { const choosenGovernance = values.essentials.governance; const freezeGuard = isSubDAO ? values.freeze : undefined; + + const valuesEssentialsWithSnapshotURL = { + ...values.essentials, + snapshotURL: values.essentials.snapshotENS, + }; + switch (choosenGovernance) { case GovernanceType.MULTISIG: { const data = await prepareMultisigFormData({ - ...values.essentials, + ...valuesEssentialsWithSnapshotURL, ...values.multisig, freezeGuard, }); @@ -43,7 +49,7 @@ function DaoCreator({ } case GovernanceType.AZORIUS_ERC20: { const data = await prepareAzoriusERC20FormData({ - ...values.essentials, + ...valuesEssentialsWithSnapshotURL, ...values.azorius, ...values.erc20Token, freezeGuard, @@ -55,7 +61,7 @@ function DaoCreator({ } case GovernanceType.AZORIUS_ERC721: { const data = await prepareAzoriusERC721FormData({ - ...values.essentials, + ...valuesEssentialsWithSnapshotURL, ...values.azorius, ...values.erc721Token, freezeGuard, diff --git a/src/components/Proposals/ProposalInfo.tsx b/src/components/Proposals/ProposalInfo.tsx index d36cef9a8a..4241c6eb4e 100644 --- a/src/components/Proposals/ProposalInfo.tsx +++ b/src/components/Proposals/ProposalInfo.tsx @@ -36,7 +36,7 @@ export function ProposalInfo({ {isSnapshotProposal && ( <> {(proposal as ExtendedSnapshotProposal).privacy === 'shutter' && ( diff --git a/src/components/pages/DaoSettings/components/Metadata/index.tsx b/src/components/pages/DaoSettings/components/Metadata/index.tsx index 2e574bf77f..831fc68078 100644 --- a/src/components/pages/DaoSettings/components/Metadata/index.tsx +++ b/src/components/pages/DaoSettings/components/Metadata/index.tsx @@ -41,7 +41,7 @@ export default function MetadataContainer() { } }, [daoName, daoSnapshotENS, daoAddress]); - const handleSnapshotURLChange: ChangeEventHandler = e => { + const handleSnapshotENSChange: ChangeEventHandler = e => { setSnapshotENS(e.target.value); try { ens_normalize(e.target.value); @@ -102,7 +102,7 @@ export default function MetadataContainer() { values: [0n], calldatas: [ keyValuePairsContract.asProvider.interface.encodeFunctionData('updateValues', [ - ['snapshotURL'], + ['snapshotENS'], [snapshotENS], ]), ], @@ -192,11 +192,11 @@ export default function MetadataContainer() { )} - {node.daoSnapshotENS && } + {node.daoSnapshotENS && } ); diff --git a/src/hooks/DAO/loaders/snapshot/useSnapshotProposals.ts b/src/hooks/DAO/loaders/snapshot/useSnapshotProposals.ts index cdfd12ae19..c2ae4e63f2 100644 --- a/src/hooks/DAO/loaders/snapshot/useSnapshotProposals.ts +++ b/src/hooks/DAO/loaders/snapshot/useSnapshotProposals.ts @@ -10,7 +10,7 @@ import { createClient } from './'; export const useSnapshotProposals = () => { const { action } = useFractal(); const daoSnapshotSpaceName = useSnapshotSpaceName(); - const currentSnapshotURL = useRef(); + const currentSnapshotENS = useRef(); const client = useMemo(() => createClient(), []); const loadSnapshotProposals = useCallback(async () => { @@ -76,8 +76,8 @@ export const useSnapshotProposals = () => { }, [action, daoSnapshotSpaceName, client]); useEffect(() => { - if (!daoSnapshotSpaceName || daoSnapshotSpaceName === currentSnapshotURL.current) return; - currentSnapshotURL.current = daoSnapshotSpaceName; + if (!daoSnapshotSpaceName || daoSnapshotSpaceName === currentSnapshotENS.current) return; + currentSnapshotENS.current = daoSnapshotSpaceName; loadSnapshotProposals(); }, [daoSnapshotSpaceName, loadSnapshotProposals]); }; diff --git a/src/hooks/DAO/loaders/useFractalNode.ts b/src/hooks/DAO/loaders/useFractalNode.ts index e4458612a7..ceb59f5086 100644 --- a/src/hooks/DAO/loaders/useFractalNode.ts +++ b/src/hooks/DAO/loaders/useFractalNode.ts @@ -40,7 +40,13 @@ export const useFractalNode = ( const { daos } = result.data; const dao = daos[0]; if (dao) { - const { parentAddress, name, hierarchy, snapshotURL, proposalTemplatesHash } = dao; + const { + parentAddress, + name, + hierarchy, + snapshotURL: snapshotENS, + proposalTemplatesHash, + } = dao; const currentNode: Node = { nodeHierarchy: { @@ -49,7 +55,7 @@ export const useFractalNode = ( }, daoName: name as string, daoAddress: utils.getAddress(_daoAddress as string), - daoSnapshotENS: snapshotURL as string, + daoSnapshotENS: snapshotENS as string, proposalTemplatesHash: proposalTemplatesHash as string, }; return currentNode; diff --git a/src/hooks/DAO/loaders/useLoadDAONode.ts b/src/hooks/DAO/loaders/useLoadDAONode.ts index cf4ecaff33..d20fceb44d 100644 --- a/src/hooks/DAO/loaders/useLoadDAONode.ts +++ b/src/hooks/DAO/loaders/useLoadDAONode.ts @@ -30,7 +30,7 @@ export const useLoadDAONode = () => { const { daos } = result.data; const dao = daos[0]; if (dao) { - const { parentAddress, name, hierarchy, snapshotURL } = dao; + const { parentAddress, name, hierarchy, snapshotURL: snapshotENS } = dao; const currentNode: Node = { nodeHierarchy: { @@ -39,7 +39,7 @@ export const useLoadDAONode = () => { }, daoName: name as string, daoAddress: utils.getAddress(_daoAddress as string), - daoSnapshotENS: snapshotURL as string, + daoSnapshotENS: snapshotENS as string, }; return currentNode; } diff --git a/src/hooks/schemas/DAOCreate/useDAOCreateSchema.ts b/src/hooks/schemas/DAOCreate/useDAOCreateSchema.ts index 934da55988..c2b83b0888 100644 --- a/src/hooks/schemas/DAOCreate/useDAOCreateSchema.ts +++ b/src/hooks/schemas/DAOCreate/useDAOCreateSchema.ts @@ -34,7 +34,7 @@ export const useDAOCreateSchema = ({ isSubDAO }: { isSubDAO?: boolean }) => { essentials: Yup.object().shape({ daoName: Yup.string().required(), governance: Yup.string().required(), - snapshotURL: Yup.string(), + snapshotENS: Yup.string(), }), multisig: Yup.object().when('essentials', { is: ({ governance }: DAOEssentials) => governance === GovernanceType.MULTISIG, diff --git a/src/types/createDAO.ts b/src/types/createDAO.ts index 21cbf986b7..44683e3fbf 100644 --- a/src/types/createDAO.ts +++ b/src/types/createDAO.ts @@ -38,9 +38,19 @@ export interface CreatorFormState { export type DAOEssentials = { daoName: string; governance: GovernanceType; - snapshotURL: string; + snapshotENS: string; }; +/** +* `DAOEssentialsEdge` is a transitionary type that is used in place of the in-app-only `DAOEssentials` type. +* `DAOEssentialsEdge` has a `snapshotURL` field in place of a `snapshotENS` field. +* +* A recent update necessitated the renaming of references to `snapshotURL` to `snapshotENS`, +* but as the contracts and subgraph already use `snapshotURL`, this type is used to maintain compatibility +* of the app with the contracts and subgraph of the outside world. +*/ +type DAOEssentialsEdge = Omit & { snapshotURL: string }; + export type DAOGovernorERC20Token = { tokenCreationType: TokenCreationType; tokenImportAddress?: string; @@ -90,7 +100,7 @@ export interface SubDAO DAOFreezeGuardConfig {} export interface AzoriusGovernanceDAO - extends DAOEssentials, + extends DAOEssentialsEdge, DAOGovernorModuleConfig {} export interface AzoriusERC20DAO @@ -104,7 +114,7 @@ export interface AzoriusERC721DAO extends AzoriusGovernanceDAO, DAOGovernorERC721Token {} -export interface SafeMultisigDAO extends DAOEssentials, SafeConfiguration {} +export interface SafeMultisigDAO extends DAOEssentialsEdge, SafeConfiguration {} export type DAOTrigger = ( daoData: SafeMultisigDAO | AzoriusERC20DAO | AzoriusERC721DAO | SubDAO, diff --git a/src/types/fractal.ts b/src/types/fractal.ts index 0aaf582bb9..9a984807b6 100644 --- a/src/types/fractal.ts +++ b/src/types/fractal.ts @@ -132,13 +132,13 @@ export enum FractalProposalState { /** * The proposal is pending, meaning it has been created, but voting has not yet begun. This state * has nothing to do with Fractal, and is used for Snapshot proposals only, which appear if the - * DAO's snapshotURL is set. + * DAO's snapshotENS is set. */ PENDING = 'statePending', /** * The proposal is closed, and no longer able to be signed. This state has nothing to do with Fractal, - * and is used for Snapshot proposals only, which appear if the DAO's snapshotURL is set. + * and is used for Snapshot proposals only, which appear if the DAO's snapshotENS is set. */ CLOSED = 'stateClosed', } From ed561a889b974f2b2804faaa8bcc5a98b7a95f4e Mon Sep 17 00:00:00 2001 From: Kellar Date: Wed, 17 Apr 2024 12:44:00 +0100 Subject: [PATCH 48/55] Update snaphshotGraphQlClient name --- src/hooks/DAO/loaders/snapshot/index.ts | 2 +- .../loaders/snapshot/useSnapshotProposal.ts | 23 +++++++++++-------- .../loaders/snapshot/useSnapshotProposals.ts | 10 ++++---- src/types/createDAO.ts | 16 ++++++------- 4 files changed, 28 insertions(+), 23 deletions(-) diff --git a/src/hooks/DAO/loaders/snapshot/index.ts b/src/hooks/DAO/loaders/snapshot/index.ts index 193bb2fd4d..1740bb2e90 100644 --- a/src/hooks/DAO/loaders/snapshot/index.ts +++ b/src/hooks/DAO/loaders/snapshot/index.ts @@ -11,7 +11,7 @@ const defaultOptions: DefaultOptions = { }, }; -export const createClient = () => +export const createSnapshotGraphQlClient = () => new ApolloClient({ uri: 'https://hub.snapshot.org/graphql', cache: new InMemoryCache(), diff --git a/src/hooks/DAO/loaders/snapshot/useSnapshotProposal.ts b/src/hooks/DAO/loaders/snapshot/useSnapshotProposal.ts index adb96e3aed..726524207e 100644 --- a/src/hooks/DAO/loaders/snapshot/useSnapshotProposal.ts +++ b/src/hooks/DAO/loaders/snapshot/useSnapshotProposal.ts @@ -11,7 +11,7 @@ import { SnapshotWeightedVotingChoice, } from '../../../../types'; import useSnapshotSpaceName from './useSnapshotSpaceName'; -import { createClient } from './'; +import { createSnapshotGraphQlClient } from './'; export default function useSnapshotProposal(proposal: FractalProposal | null | undefined) { const [extendedSnapshotProposal, setExtendedSnapshotProposal] = @@ -22,7 +22,7 @@ export default function useSnapshotProposal(proposal: FractalProposal | null | u }, } = useFractal(); const daoSnapshotSpaceName = useSnapshotSpaceName(); - const client = useMemo(() => createClient(), []); + const snaphshotGraphQlClient = useMemo(() => createSnapshotGraphQlClient(), []); const snapshotProposal = proposal as SnapshotProposal; const isSnapshotProposal = useMemo( @@ -31,8 +31,8 @@ export default function useSnapshotProposal(proposal: FractalProposal | null | u ); const loadProposal = useCallback(async () => { - if (snapshotProposal?.snapshotProposalId && client) { - const proposalQueryResult = await client + if (snapshotProposal?.snapshotProposalId && snaphshotGraphQlClient) { + const proposalQueryResult = await snaphshotGraphQlClient .query({ query: gql` query ExtendedSnapshotProposal { @@ -72,7 +72,7 @@ export default function useSnapshotProposal(proposal: FractalProposal | null | u }, ); - const votesQueryResult = await client + const votesQueryResult = await snaphshotGraphQlClient .query({ query: gql`query SnapshotProposalVotes { votes(where: {proposal: "${snapshotProposal.snapshotProposalId}"}, first: 500) { @@ -194,7 +194,12 @@ export default function useSnapshotProposal(proposal: FractalProposal | null | u votes: votesQueryResult, } as ExtendedSnapshotProposal); } - }, [snapshotProposal?.snapshotProposalId, proposal, snapshotProposal?.state, client]); + }, [ + snapshotProposal?.snapshotProposalId, + proposal, + snapshotProposal?.state, + snaphshotGraphQlClient, + ]); const loadVotingWeight = useCallback(async () => { const emptyVotingWeight = { @@ -202,8 +207,8 @@ export default function useSnapshotProposal(proposal: FractalProposal | null | u votingWeightByStrategy: [0], votingState: '', }; - if (snapshotProposal?.snapshotProposalId && client) { - const queryResult = await client + if (snapshotProposal?.snapshotProposalId && snaphshotGraphQlClient) { + const queryResult = await snaphshotGraphQlClient .query({ query: gql` query UserVotingWeight { @@ -234,7 +239,7 @@ export default function useSnapshotProposal(proposal: FractalProposal | null | u } return emptyVotingWeight; - }, [address, snapshotProposal?.snapshotProposalId, client, daoSnapshotSpaceName]); + }, [address, snapshotProposal?.snapshotProposalId, snaphshotGraphQlClient, daoSnapshotSpaceName]); return { loadVotingWeight, diff --git a/src/hooks/DAO/loaders/snapshot/useSnapshotProposals.ts b/src/hooks/DAO/loaders/snapshot/useSnapshotProposals.ts index c2ae4e63f2..7bf8d495e9 100644 --- a/src/hooks/DAO/loaders/snapshot/useSnapshotProposals.ts +++ b/src/hooks/DAO/loaders/snapshot/useSnapshotProposals.ts @@ -5,17 +5,17 @@ import { FractalGovernanceAction } from '../../../../providers/App/governance/ac import { ActivityEventType, FractalProposalState } from '../../../../types'; import { SnapshotProposal } from '../../../../types/daoProposal'; import useSnapshotSpaceName from './useSnapshotSpaceName'; -import { createClient } from './'; +import { createSnapshotGraphQlClient } from './'; export const useSnapshotProposals = () => { const { action } = useFractal(); const daoSnapshotSpaceName = useSnapshotSpaceName(); const currentSnapshotENS = useRef(); - const client = useMemo(() => createClient(), []); + const snaphshotGraphQlClient = useMemo(() => createSnapshotGraphQlClient(), []); const loadSnapshotProposals = useCallback(async () => { - if (client) { - client + if (snaphshotGraphQlClient) { + snaphshotGraphQlClient .query({ query: gql` query Proposals { @@ -73,7 +73,7 @@ export const useSnapshotProposals = () => { }); }); } - }, [action, daoSnapshotSpaceName, client]); + }, [action, daoSnapshotSpaceName, snaphshotGraphQlClient]); useEffect(() => { if (!daoSnapshotSpaceName || daoSnapshotSpaceName === currentSnapshotENS.current) return; diff --git a/src/types/createDAO.ts b/src/types/createDAO.ts index 44683e3fbf..9b5dd926c8 100644 --- a/src/types/createDAO.ts +++ b/src/types/createDAO.ts @@ -41,14 +41,14 @@ export type DAOEssentials = { snapshotENS: string; }; -/** -* `DAOEssentialsEdge` is a transitionary type that is used in place of the in-app-only `DAOEssentials` type. -* `DAOEssentialsEdge` has a `snapshotURL` field in place of a `snapshotENS` field. -* -* A recent update necessitated the renaming of references to `snapshotURL` to `snapshotENS`, -* but as the contracts and subgraph already use `snapshotURL`, this type is used to maintain compatibility -* of the app with the contracts and subgraph of the outside world. -*/ +/** + * `DAOEssentialsEdge` is a transitionary type that is used in place of the in-app-only `DAOEssentials` type. + * `DAOEssentialsEdge` has a `snapshotURL` field in place of a `snapshotENS` field. + * + * A recent update necessitated the renaming of references to `snapshotURL` to `snapshotENS`, + * but as the contracts and subgraph already use `snapshotURL`, this type is used to maintain compatibility + * of the app with the contracts and subgraph of the outside world. + */ type DAOEssentialsEdge = Omit & { snapshotURL: string }; export type DAOGovernorERC20Token = { From ff4f7dd2d1f6812d538f30eddf3b617df86bf9d4 Mon Sep 17 00:00:00 2001 From: Kellar Date: Wed, 17 Apr 2024 12:50:23 +0100 Subject: [PATCH 49/55] Slight code style update --- .../DaoCreator/formComponents/EstablishEssentials.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/components/DaoCreator/formComponents/EstablishEssentials.tsx b/src/components/DaoCreator/formComponents/EstablishEssentials.tsx index 48e255c8ed..f10ea3a58d 100644 --- a/src/components/DaoCreator/formComponents/EstablishEssentials.tsx +++ b/src/components/DaoCreator/formComponents/EstablishEssentials.tsx @@ -66,13 +66,19 @@ export function EstablishEssentials(props: ICreationStepProps) { const handleSnapshotSpaceChange = (value: string) => { setFieldValue('essentials.snapshotENS', value, true); + + // If there's no input in the snapshot URL field, we don't need to check if it's valid + if (!value) { + setSnapshotSpaceValid(true); + return; + } + try { ens_normalize(value); setSnapshotSpaceValid(true); } catch (error) { console.log(error); - // If there's no input in the snapshot URL field, we don't need to check if it's valid - setSnapshotSpaceValid(!!value ? false : true); + setSnapshotSpaceValid(false); } }; From 42f2a5dd38d19b15dc32d4dd5b4e340598f785d3 Mon Sep 17 00:00:00 2001 From: Kellar Date: Wed, 17 Apr 2024 12:51:05 +0100 Subject: [PATCH 50/55] isSnapshotSpaceValid should start out valid --- .../DaoCreator/formComponents/EstablishEssentials.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/DaoCreator/formComponents/EstablishEssentials.tsx b/src/components/DaoCreator/formComponents/EstablishEssentials.tsx index f10ea3a58d..b28dd61342 100644 --- a/src/components/DaoCreator/formComponents/EstablishEssentials.tsx +++ b/src/components/DaoCreator/formComponents/EstablishEssentials.tsx @@ -30,7 +30,7 @@ export function EstablishEssentials(props: ICreationStepProps) { const { t } = useTranslation(['daoCreate', 'common']); const { values, setFieldValue, isSubmitting, transactionPending, isSubDAO, errors, mode } = props; - const [isSnapshotSpaceValid, setSnapshotSpaceValid] = useState(false); + const [isSnapshotSpaceValid, setSnapshotSpaceValid] = useState(true); const { node: { daoName, daoSnapshotENS, daoAddress }, From d4af81425545da6c44079743a2b2c32e68fd97dc Mon Sep 17 00:00:00 2001 From: Kellar Date: Wed, 17 Apr 2024 12:52:58 +0100 Subject: [PATCH 51/55] prettier --- .../DaoCreator/formComponents/EstablishEssentials.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/DaoCreator/formComponents/EstablishEssentials.tsx b/src/components/DaoCreator/formComponents/EstablishEssentials.tsx index b28dd61342..0349479ea5 100644 --- a/src/components/DaoCreator/formComponents/EstablishEssentials.tsx +++ b/src/components/DaoCreator/formComponents/EstablishEssentials.tsx @@ -68,10 +68,10 @@ export function EstablishEssentials(props: ICreationStepProps) { setFieldValue('essentials.snapshotENS', value, true); // If there's no input in the snapshot URL field, we don't need to check if it's valid - if (!value) { - setSnapshotSpaceValid(true); - return; - } + if (!value) { + setSnapshotSpaceValid(true); + return; + } try { ens_normalize(value); From deff8d60bf8a7dbaec942d8adabd74c8b6c22082 Mon Sep 17 00:00:00 2001 From: Adam Gall Date: Wed, 17 Apr 2024 09:03:03 -0400 Subject: [PATCH 52/55] Finish removing "snapshotURL" from the app everywhere except the edges --- src/components/DaoCreator/index.tsx | 11 +++-------- src/models/DaoTxBuilder.ts | 2 +- .../daos/[daoAddress]/edit/governance/index.tsx | 2 +- src/types/createDAO.ts | 14 ++------------ 4 files changed, 7 insertions(+), 22 deletions(-) diff --git a/src/components/DaoCreator/index.tsx b/src/components/DaoCreator/index.tsx index e49f575cf7..646cdd6eeb 100644 --- a/src/components/DaoCreator/index.tsx +++ b/src/components/DaoCreator/index.tsx @@ -30,15 +30,10 @@ function DaoCreator({ const choosenGovernance = values.essentials.governance; const freezeGuard = isSubDAO ? values.freeze : undefined; - const valuesEssentialsWithSnapshotURL = { - ...values.essentials, - snapshotURL: values.essentials.snapshotENS, - }; - switch (choosenGovernance) { case GovernanceType.MULTISIG: { const data = await prepareMultisigFormData({ - ...valuesEssentialsWithSnapshotURL, + ...values.essentials, ...values.multisig, freezeGuard, }); @@ -49,7 +44,7 @@ function DaoCreator({ } case GovernanceType.AZORIUS_ERC20: { const data = await prepareAzoriusERC20FormData({ - ...valuesEssentialsWithSnapshotURL, + ...values.essentials, ...values.azorius, ...values.erc20Token, freezeGuard, @@ -61,7 +56,7 @@ function DaoCreator({ } case GovernanceType.AZORIUS_ERC721: { const data = await prepareAzoriusERC721FormData({ - ...valuesEssentialsWithSnapshotURL, + ...values.essentials, ...values.azorius, ...values.erc721Token, freezeGuard, diff --git a/src/models/DaoTxBuilder.ts b/src/models/DaoTxBuilder.ts index 1b64e2d568..723b78b094 100644 --- a/src/models/DaoTxBuilder.ts +++ b/src/models/DaoTxBuilder.ts @@ -231,7 +231,7 @@ export class DaoTxBuilder extends BaseTxBuilder { return buildContractCall( this.baseContracts.keyValuePairsContract, 'updateValues', - [['snapshotURL'], [this.daoData.snapshotURL]], + [['snapshotURL'], [this.daoData.snapshotENS]], 0, false, ); diff --git a/src/pages/daos/[daoAddress]/edit/governance/index.tsx b/src/pages/daos/[daoAddress]/edit/governance/index.tsx index 0a1a7475ce..81d45f7550 100644 --- a/src/pages/daos/[daoAddress]/edit/governance/index.tsx +++ b/src/pages/daos/[daoAddress]/edit/governance/index.tsx @@ -36,7 +36,7 @@ export default function ModifyGovernancePage() { deployAzorius( daoData as AzoriusERC20DAO | AzoriusERC721DAO, !daoName || createAccountSubstring(daoAddress!) === daoName, - !daoSnapshotENS && !!daoData.snapshotURL, + !daoSnapshotENS && !!daoData.snapshotENS, ); }; diff --git a/src/types/createDAO.ts b/src/types/createDAO.ts index 9b5dd926c8..9d68f47b75 100644 --- a/src/types/createDAO.ts +++ b/src/types/createDAO.ts @@ -41,16 +41,6 @@ export type DAOEssentials = { snapshotENS: string; }; -/** - * `DAOEssentialsEdge` is a transitionary type that is used in place of the in-app-only `DAOEssentials` type. - * `DAOEssentialsEdge` has a `snapshotURL` field in place of a `snapshotENS` field. - * - * A recent update necessitated the renaming of references to `snapshotURL` to `snapshotENS`, - * but as the contracts and subgraph already use `snapshotURL`, this type is used to maintain compatibility - * of the app with the contracts and subgraph of the outside world. - */ -type DAOEssentialsEdge = Omit & { snapshotURL: string }; - export type DAOGovernorERC20Token = { tokenCreationType: TokenCreationType; tokenImportAddress?: string; @@ -100,7 +90,7 @@ export interface SubDAO DAOFreezeGuardConfig {} export interface AzoriusGovernanceDAO - extends DAOEssentialsEdge, + extends DAOEssentials, DAOGovernorModuleConfig {} export interface AzoriusERC20DAO @@ -114,7 +104,7 @@ export interface AzoriusERC721DAO extends AzoriusGovernanceDAO, DAOGovernorERC721Token {} -export interface SafeMultisigDAO extends DAOEssentialsEdge, SafeConfiguration {} +export interface SafeMultisigDAO extends DAOEssentials, SafeConfiguration {} export type DAOTrigger = ( daoData: SafeMultisigDAO | AzoriusERC20DAO | AzoriusERC721DAO | SubDAO, From 417356a6fbeb70df585e7785d0dcf7c7ff54ff83 Mon Sep 17 00:00:00 2001 From: Kellar Date: Wed, 17 Apr 2024 16:42:44 +0100 Subject: [PATCH 53/55] Fix dao settings snapshot ens placeholder --- src/components/pages/DaoSettings/components/Metadata/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/pages/DaoSettings/components/Metadata/index.tsx b/src/components/pages/DaoSettings/components/Metadata/index.tsx index 831fc68078..a73f123d3d 100644 --- a/src/components/pages/DaoSettings/components/Metadata/index.tsx +++ b/src/components/pages/DaoSettings/components/Metadata/index.tsx @@ -195,7 +195,7 @@ export default function MetadataContainer() { onChange={handleSnapshotENSChange} value={snapshotENS} disabled={!userHasVotingWeight} - placeholder="httpsexample.eth" + placeholder="example.eth" testId="daoSettings.snapshotENS" gridContainerProps={{ display: 'inline-flex', From b513c9007b7f668c0695359e95e6b8d9af31563f Mon Sep 17 00:00:00 2001 From: Kellar Date: Wed, 17 Apr 2024 16:43:27 +0100 Subject: [PATCH 54/55] remove unused import --- src/components/ui/modals/ProposalTemplateModal.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/ui/modals/ProposalTemplateModal.tsx b/src/components/ui/modals/ProposalTemplateModal.tsx index 6cc11a8431..71791f8631 100644 --- a/src/components/ui/modals/ProposalTemplateModal.tsx +++ b/src/components/ui/modals/ProposalTemplateModal.tsx @@ -8,7 +8,6 @@ import { Switch, VStack, } from '@chakra-ui/react'; -import { utils } from 'ethers'; import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; From 5ad1737e89a625a3a4f1b8bdb3a8282310af4c18 Mon Sep 17 00:00:00 2001 From: Adam Gall Date: Wed, 17 Apr 2024 19:00:02 -0400 Subject: [PATCH 55/55] 2024-04-17 release