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, + }; +};