From 85337f955cd4b3a08c07e3ad00acad01f2200ac8 Mon Sep 17 00:00:00 2001 From: Carlos Fontes Date: Thu, 19 Sep 2024 15:44:48 +0100 Subject: [PATCH] feat: now user can create safe proposal for payout --- packages/web/src/languages/en.json | 2 + .../PayoutStatusPage/PayoutStatusPage.tsx | 46 ++++++++++++++- .../useCreatePayoutProposal.ts | 57 +++++++++++++++++++ 3 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 packages/web/src/pages/CommitteeTools/PayoutsTool/PayoutStatusPage/useCreatePayoutProposal.ts diff --git a/packages/web/src/languages/en.json b/packages/web/src/languages/en.json index 66d069273..ebce9c6a9 100644 --- a/packages/web/src/languages/en.json +++ b/packages/web/src/languages/en.json @@ -1271,6 +1271,8 @@ "paymentPerPoint": "Payment per point", "maxPercentagePerPoint": "Max. percentage per point (%)", "maxPercentagePerPointPlaceholder": "Max. % of the vault allocated to one point", + "creatingSafeProposal": "Creating safe proposal", + "createProposalOnSafe": "Create proposal on Safe", "payoutStatusDescriptions": { "pending": "Your payout multisig transaction was created please share this page between other committee member to sign it. Once all signatures are collected you will be able to execute the payout transaction.", "readyToExecute": "The needed signatures were collected, you can now execute the payout transaction. Please be aware that the payout can only be executed during safety period.", diff --git a/packages/web/src/pages/CommitteeTools/PayoutsTool/PayoutStatusPage/PayoutStatusPage.tsx b/packages/web/src/pages/CommitteeTools/PayoutsTool/PayoutStatusPage/PayoutStatusPage.tsx index 21c38646e..da1d74809 100644 --- a/packages/web/src/pages/CommitteeTools/PayoutsTool/PayoutStatusPage/PayoutStatusPage.tsx +++ b/packages/web/src/pages/CommitteeTools/PayoutsTool/PayoutStatusPage/PayoutStatusPage.tsx @@ -2,6 +2,8 @@ import { HATSVaultV2_abi, HATSVaultV3ClaimsManager_abi, PayoutStatus, + getBaseSafeAppUrl, + getGnosisChainPrefixByChainId, getSafeHomeLink, getVaultInfoWithCommittee, isAddressAMultisigMember, @@ -31,7 +33,7 @@ import { useVaultSafeInfo } from "hooks/vaults/useVaultSafeInfo"; import { RoutePaths } from "navigation"; import { useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; -import { useNavigate, useParams } from "react-router-dom"; +import { useNavigate, useParams, useSearchParams } from "react-router-dom"; import { switchNetworkAndValidate } from "utils/switchNetwork.utils"; import { useAccount, useNetwork, useWaitForTransaction } from "wagmi"; import { PayoutsWelcome } from "../PayoutsListPage/PayoutsWelcome"; @@ -39,6 +41,7 @@ import { PayoutCard, SignerCard, SinglePayoutAllocation, SplitPayoutAllocation } import { useAddSignature, useDeletePayout, useMarkPayoutAsExecuted, usePayout } from "../payoutsService.hooks"; import { usePayoutStatus } from "../utils/usePayoutStatus"; import { StyledPayoutStatusPage } from "./styles"; +import { useCreatePayoutProposal } from "./useCreatePayoutProposal"; import { useSignPayout } from "./useSignPayout"; const DELETABLE_STATUS = [PayoutStatus.Creating, PayoutStatus.Pending, PayoutStatus.ReadyToExecute]; @@ -52,6 +55,10 @@ export const PayoutStatusPage = () => { const confirm = useConfirm(); const { tryAuthentication, isAuthenticated } = useSiweAuth(); + const [searchParams] = useSearchParams(); + const isAdvancedMode = searchParams.get("mode")?.includes("advanced") ?? false; + const [proposalCreatedSuccessfully, setProposalCreatedSuccessfully] = useState(); + const { allVaults, allPayouts, withdrawSafetyPeriod } = useVaults(); const { payoutId } = useParams(); @@ -68,6 +75,7 @@ export const PayoutStatusPage = () => { const addSignature = useAddSignature(); const markPayoutAsExecuted = useMarkPayoutAsExecuted(); const signPayout = useSignPayout(vault, payout); + const createPayoutProposal = useCreatePayoutProposal(vault, payout); const executePayout = ExecutePayoutContract.hook(vault, payout); const waitingPayoutExecution = useWaitForTransaction({ hash: executePayout.data?.hash as `0x${string}`, @@ -98,6 +106,7 @@ export const PayoutStatusPage = () => { const isCollectingSignatures = payoutStatus === PayoutStatus.Pending; const canBeDeleted = payoutStatus && DELETABLE_STATUS.includes(payoutStatus); const canBesigned = payoutStatus && SIGNABLE_STATUS.includes(payoutStatus); + const canCreateProposalOnSafe = isAdvancedMode; const isAnyActivePayout = allPayouts?.some((payout) => payout.vault.id === vault?.id && payout.isActive); const [isUserCommitteeMember, setIsUserCommitteeMember] = useState(false); @@ -174,6 +183,24 @@ export const PayoutStatusPage = () => { refetchPayout(); }; + const handleCreateProposalOnSafe = async () => { + if (!isUserCommitteeMember) return; + if (!payoutId || !payout || !vault) return; + + const isOk = await createPayoutProposal.create(); + setProposalCreatedSuccessfully(isOk); + }; + + const goToSafeApp = () => { + if (!vault) return; + + const multisig = vault.committee; + window.open( + `${getBaseSafeAppUrl(vault.chainId)}/transactions/queue?safe=${getGnosisChainPrefixByChainId(vault.chainId)}:${multisig}`, + "_blank" + ); + }; + const handleExecutePayout = async () => { if (!withdrawSafetyPeriod?.isSafetyPeriod || !isReadyToExecute || !payout || isAnyActivePayout) return; await executePayout.send(); @@ -348,6 +375,11 @@ export const PayoutStatusPage = () => { )}
+ {canCreateProposalOnSafe && ( + + )} {canBesigned && !userHasAlreadySigned && (
+ + {proposalCreatedSuccessfully && ( + + <> + {t("proposalCreatedSuccessfully")} + + + + )} )} @@ -369,6 +412,7 @@ export const PayoutStatusPage = () => { {(addSignature.isLoading || signPayout.isLoading) && ( )} + {createPayoutProposal.isLoading && } {(executePayout.isLoading || waitingPayoutExecution.isLoading || markPayoutAsExecuted.isLoading) && ( )} diff --git a/packages/web/src/pages/CommitteeTools/PayoutsTool/PayoutStatusPage/useCreatePayoutProposal.ts b/packages/web/src/pages/CommitteeTools/PayoutsTool/PayoutStatusPage/useCreatePayoutProposal.ts new file mode 100644 index 000000000..182849862 --- /dev/null +++ b/packages/web/src/pages/CommitteeTools/PayoutsTool/PayoutStatusPage/useCreatePayoutProposal.ts @@ -0,0 +1,57 @@ +import { IPayoutResponse, IVault, getExecutePayoutSafeTransaction, getGnosisSafeTxServiceBaseUrl } from "@hats.finance/shared"; +import SafeApiKit from "@safe-global/api-kit"; +import Safe, { EthersAdapter } from "@safe-global/protocol-kit"; +import { Signer, ethers, utils } from "ethers"; +import { useState } from "react"; +import { useAccount, useProvider, useSigner } from "wagmi"; + +export const useCreatePayoutProposal = (vault?: IVault, payout?: IPayoutResponse) => { + const { address: account } = useAccount(); + const provider = useProvider(); + const { data: signer } = useSigner(); + + const [isLoading, setIsLoading] = useState(false); + + const create = async () => { + try { + if (!vault || !payout || !account) return; + setIsLoading(true); + + const multisigAddress = utils.getAddress(vault.committee ?? ""); + if (!multisigAddress) { + alert("No vault multisig address. Please contact Hats team with this error."); + return false; + } + + const ethAdapter = new EthersAdapter({ ethers, signerOrProvider: signer as Signer }); + const txServiceUrl = getGnosisSafeTxServiceBaseUrl(vault.chainId); + const safeService = new SafeApiKit({ txServiceUrl, ethAdapter }); + const safeSdk = await Safe.create({ ethAdapter, safeAddress: multisigAddress }); + + const { tx: safeTransaction } = await getExecutePayoutSafeTransaction(provider, multisigAddress, payout); + + const safeTxHash = await safeSdk.getTransactionHash(safeTransaction); + const senderSignature = await safeSdk.signTypedData(safeTransaction); + await safeService.proposeTransaction({ + safeAddress: multisigAddress, + safeTransactionData: safeTransaction.data, + safeTxHash, + senderAddress: account, + senderSignature: senderSignature.data, + origin: "https://app.hats.finance", + }); + + return true; + } catch (error) { + console.log(error); + return false; + } finally { + setIsLoading(false); + } + }; + + return { + create, + isLoading, + }; +};