diff --git a/apps/davi/public/locales/en/translation.json b/apps/davi/public/locales/en/translation.json index 382f8308..ad8c3299 100644 --- a/apps/davi/public/locales/en/translation.json +++ b/apps/davi/public/locales/en/translation.json @@ -266,12 +266,14 @@ "invalidContentHash": "Invalid content hash", "titleRequired": "Title is required", "atLeastOneOptionRequired": "At least one option is required", + "mustBe2Options": "There must be 2 options", "atLeastOneActionPerOptionRequired": "At least one action per option is required", "optionsArrayIsEmpty": "Options array is empty", "metadataUploadError": "Metadata upload error", "genericProposalError": "We ran into an error.", "couldntFindTheProposal": "We couldn't find that proposal", - "probablyNonExistent": "It probably doesn't exist" + "probablyNonExistent": "It probably doesn't exist", + "errorFetchingScheme": "We ran into an error fetching the dao's Scheme" } }, "tokenPicker": { diff --git a/apps/davi/public/locales/es/translation.json b/apps/davi/public/locales/es/translation.json index c2037129..a6bde4a2 100644 --- a/apps/davi/public/locales/es/translation.json +++ b/apps/davi/public/locales/es/translation.json @@ -264,12 +264,14 @@ "invalidContentHash": "Contenido hash inválido", "titleRequired": "El título es requerido", "atLeastOneOptionRequired": "Por lo menos una opción es requerida", + "mustBe2Options": "Deben ser 2 opciones", "atLeastOneActionPerOptionRequired": "Por lo menos una acción por opción es requerida", "optionsArrayIsEmpty": "Las opciones están vacías", "metadataUploadError": "Error en la carga de Metadatos", "genericProposalError": "Nos encontramos con un error.", "couldntFindTheProposal": "No pudimos encontrar esa propuesta", - "probablyNonExistent": "Probablemente no exista" + "probablyNonExistent": "Probablemente no exista", + "errorFetchingScheme": "Nos encontramos con un error buscando el esquema de la dao" } }, "tokenPicker": { diff --git a/apps/davi/src/Modules/Guilds/pages/CreateProposal.tsx b/apps/davi/src/Modules/Guilds/pages/CreateProposal.tsx index 076db0f0..0e295f6e 100644 --- a/apps/davi/src/Modules/Guilds/pages/CreateProposal.tsx +++ b/apps/davi/src/Modules/Guilds/pages/CreateProposal.tsx @@ -67,7 +67,7 @@ const CreateProposalPage: React.FC = () => { } = useHookStoreProvider(); const { orbis } = useOrbisContext(); - const createProposal = useCreateProposal(guildId, discussionId); + const createProposal = useCreateProposal(guildId, subdaoId, discussionId); const navigate = useNavigate(); const location = useLocation(); const { t } = useTranslation(); diff --git a/apps/davi/src/components/ActionsBuilder/OptionsList/OptionsList.tsx b/apps/davi/src/components/ActionsBuilder/OptionsList/OptionsList.tsx index c0283545..ebb7f2ad 100644 --- a/apps/davi/src/components/ActionsBuilder/OptionsList/OptionsList.tsx +++ b/apps/davi/src/components/ActionsBuilder/OptionsList/OptionsList.tsx @@ -37,6 +37,7 @@ import { AddOptionWrapper, SimulationButton } from './OptionsList.styled'; import { SimulationModal } from './SimulationModal'; import { OptionsListProps, SimulationState } from './types'; import { BigNumber } from 'ethers'; +import { useHookStoreProvider } from 'stores'; export const OptionsList: React.FC = ({ isEditable, @@ -51,6 +52,8 @@ export const OptionsList: React.FC = ({ const [clonedOptions, setClonedOptions] = useState(null); const recentlyMovedToNewContainer = useRef(false); const theme = useTheme(); + const { name: governanceImplementationName } = useHookStoreProvider(); + useEffect(() => { requestAnimationFrame(() => { recentlyMovedToNewContainer.current = false; @@ -378,7 +381,7 @@ export const OptionsList: React.FC = ({ )} - {isEditable && ( + {isEditable && governanceImplementationName !== 'Governance1_5' && ( <> diff --git a/apps/davi/src/stores/modules/1_5/writers/useCreateProposal/useCreateProposal.tsx b/apps/davi/src/stores/modules/1_5/writers/useCreateProposal/useCreateProposal.tsx index 5e5e5ef2..00c67a76 100644 --- a/apps/davi/src/stores/modules/1_5/writers/useCreateProposal/useCreateProposal.tsx +++ b/apps/davi/src/stores/modules/1_5/writers/useCreateProposal/useCreateProposal.tsx @@ -1,15 +1,33 @@ +import { createPost } from 'components/Forum'; +import { useTransactions } from 'contexts/Guilds'; +import { useOrbisContext } from 'contexts/Guilds/orbis'; +import { providers } from 'ethers'; +import { useSchemeContract } from 'hooks/Guilds/contracts/useContract'; +import usePinataIPFS from 'hooks/Guilds/ipfs/usePinataIPFS'; +import useWeb3Storage from 'hooks/Guilds/ipfs/useWeb3Storage'; import { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; import { WriterHooksInteface } from 'stores/types'; +import { isValid1_5Proposal } from 'utils'; +import { useNetwork } from 'wagmi'; type IUseCreateProposal = WriterHooksInteface['useCreateProposal']; type IHandleCreateProposal = ReturnType; -// TODO: placeholder hook to prevent crashing - export const useCreateProposal: IUseCreateProposal = ( daoAddress: string, + subdaoId?: string, discussionRef?: string ) => { + const { chain } = useNetwork(); + const { orbis } = useOrbisContext(); + const { pinToPinata } = usePinataIPFS(); + const { pinToStorage } = useWeb3Storage(); + const { t } = useTranslation(); + const { createTransaction } = useTransactions(); + + const schemeContract = useSchemeContract(subdaoId); + const handleCreateProposal: IHandleCreateProposal = useCallback( async ( title, @@ -23,10 +41,94 @@ export const useCreateProposal: IUseCreateProposal = ( handleMetadataUploadError, cb ) => { - return; + const { options } = otherFields; + + // adding the against option + totalOptions++; + + const { isValid, error } = isValid1_5Proposal({ + toArray, + dataArray, + valueArray, + totalOptions, + title, + }); + + const linkToOrbis = (receipt: providers.TransactionReceipt) => { + const link = { + title, + body: `Created Proposal: ${title}`, + context: `DAVI-${daoAddress}-${discussionRef}-proposal`, + master: receipt.logs[0].topics[1], + replyTo: null, + mentions: [], + data: { chain: chain }, + }; + createPost(orbis, link); + }; + + const uploadToIPFS = async () => { + const content = { + description: description, + voteOptions: ['', ...options.map(({ label }) => label)], + discussionRef: discussionRef, + }; + const pinataPin = pinToPinata(content).then(result => result?.IpfsHash); + const web3storagePin = pinToStorage(content); + const results = await Promise.all([pinataPin, web3storagePin]); + // TODO: Loop through array looking for at least two matching hashes when we have >2 pinning services + if (results[0] !== results[1]) { + console.warn(t('actionBuilder.ens.ipfs.hashNotTheSame'), results); + } + return `ipfs://${results[0]}`; + }; + + if (!isValid) throw new Error(error); + + if (options.length === 0) { + throw new Error(t('proposal.errors.optionsArrayIsEmpty')); + } + + let contentHash = ''; + + if (!skipMetadataUpload) { + try { + contentHash = await uploadToIPFS(); + } catch (error) { + handleMetadataUploadError(error); + return; + } + } + + if (!isValid) throw new Error(error); + + createTransaction( + `${t('createProposal.createProposal')} ${title}`, + async () => { + return schemeContract.proposeCalls( + toArray, + dataArray, + valueArray, + totalOptions, + title, + contentHash + ); + }, + true, + cb, + linkToOrbis + ); }, // eslint-disable-next-line react-hooks/exhaustive-deps - [] + [ + schemeContract, + createTransaction, + t, + pinToPinata, + discussionRef, + daoAddress, + orbis, + ] ); return handleCreateProposal; diff --git a/apps/davi/src/stores/types.ts b/apps/davi/src/stores/types.ts index 9520c55a..94005f5f 100644 --- a/apps/davi/src/stores/types.ts +++ b/apps/davi/src/stores/types.ts @@ -169,6 +169,7 @@ export interface WriterHooksInteface { ) => (daoTokenVault: string, amount?: string) => Promise; useCreateProposal: ( daoAddress: string, + subDaoAddress?: string, linkRef?: string ) => ( title: string, diff --git a/apps/davi/src/utils/validations.ts b/apps/davi/src/utils/validations.ts index 7298ed21..367e6cdd 100644 --- a/apps/davi/src/utils/validations.ts +++ b/apps/davi/src/utils/validations.ts @@ -43,6 +43,54 @@ export const isValidGuildProposal = ({ }; }; +export const isValid1_5Proposal = ({ + toArray, + dataArray, + valueArray, + totalOptions, + title, +}: { + toArray: string[]; + dataArray: string[]; + valueArray: BigNumber[]; + totalOptions: number; + title: string; +}): { isValid: boolean; error?: string } => { + if (!title) { + return { + isValid: false, + error: i18next.t('proposal.errors.titleRequired'), + }; + } + if (totalOptions === 0) { + return { + isValid: false, + error: i18next.t('proposal.errors.atLeastOneOptionRequired'), + }; + } + if (totalOptions !== 2) { + return { + isValid: false, + error: i18next.t('proposal.errors.mustBe2Options'), + }; + } + if ( + toArray.length === 0 || + dataArray.length === 0 || + valueArray.length === 0 + ) { + return { + isValid: false, + error: i18next.t('proposal.errors.atLeastOneActionPerOptionRequired'), + }; + } + + return { + isValid: true, + error: null, + }; +}; + export const isEnsName = ( name: string ): { isValid: boolean; validationError: string } => {