From 8fa14fb4d1a349cca6e961801524d3c251eaabb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8r=E2=88=82=C2=A1?= Date: Thu, 29 Aug 2024 14:30:55 +0200 Subject: [PATCH 1/2] Adding useTransactionManager() to encapsulate tx's - round 1 --- hooks/useTransactionManager.ts | 68 +++++++++++++++ .../hooks/useCreateProposal.ts | 54 +++--------- .../hooks/useProposalApprovals.ts | 19 ++--- .../hooks/useProposalApprove.ts | 82 +++++------------- .../hooks/useProposalExecute.ts | 71 +++++----------- .../hooks/usePublicKeyRegistry.ts | 59 ++++--------- .../members/hooks/useAnnounceDelegation.ts | 51 +++--------- .../multisig/hooks/useProposalApprovals.ts | 25 +++--- plugins/multisig/hooks/useProposalApprove.ts | 83 +++++-------------- 9 files changed, 188 insertions(+), 324 deletions(-) create mode 100644 hooks/useTransactionManager.ts diff --git a/hooks/useTransactionManager.ts b/hooks/useTransactionManager.ts new file mode 100644 index 00000000..59842826 --- /dev/null +++ b/hooks/useTransactionManager.ts @@ -0,0 +1,68 @@ +import { useEffect } from "react"; +import { useAlerts } from "@/context/Alerts"; +import { useWaitForTransactionReceipt, useWriteContract } from "wagmi"; + +export type TxLifecycleParams = { + onSuccessMessage?: string; + onSuccessDescription?: string; + onSuccess?: () => any; + onErrorMessage?: string; + onErrorDescription?: string; + onError?: () => any; +}; + +export function useTransactionManager(params: TxLifecycleParams) { + const { onSuccess, onError } = params; + const { writeContract, data: hash, error, status } = useWriteContract(); + const { isLoading: isConfirming, isSuccess: isConfirmed } = useWaitForTransactionReceipt({ hash }); + const { addAlert } = useAlerts(); + + useEffect(() => { + if (status === "idle" || status === "pending") { + return; + } else if (status === "error") { + if (error?.message?.startsWith("User rejected the request")) { + addAlert("The transaction signature was declined", { + description: "Nothing has been sent to the network", + timeout: 4 * 1000, + }); + } else { + console.error(error); + addAlert(params.onErrorMessage || "Could not fulfill the transaction", { + type: "error", + description: params.onErrorDescription, + }); + } + + if (typeof onError === "function") { + onError(); + } + return; + } + + // TX submitted + if (!hash) { + return; + } else if (isConfirming) { + addAlert("Transaction submitted", { + description: "Waiting for the transaction to be validated", + txHash: hash, + }); + return; + } else if (!isConfirmed) { + return; + } + + addAlert(params.onSuccessMessage || "Transaction fulfilled", { + description: params.onSuccessDescription || "The transaction has been validated on the network", + type: "success", + txHash: hash, + }); + + if (typeof onSuccess === "function") { + onSuccess(); + } + }, [status, hash, isConfirming, isConfirmed]); + + return { writeContract, hash, status, isConfirming, isConfirmed }; +} diff --git a/plugins/emergency-multisig/hooks/useCreateProposal.ts b/plugins/emergency-multisig/hooks/useCreateProposal.ts index bdd8d176..43b2635f 100644 --- a/plugins/emergency-multisig/hooks/useCreateProposal.ts +++ b/plugins/emergency-multisig/hooks/useCreateProposal.ts @@ -1,8 +1,7 @@ import { useRouter } from "next/router"; import { useEncryptedData } from "./useEncryptedData"; -import { useEffect, useState } from "react"; +import { useState } from "react"; import { ProposalMetadata, RawAction } from "@/utils/types"; -import { useWaitForTransactionReceipt, useWriteContract } from "wagmi"; import { useAlerts } from "@/context/Alerts"; import { PUB_APP_NAME, @@ -15,6 +14,7 @@ import { uploadToPinata } from "@/utils/ipfs"; import { EmergencyMultisigPluginAbi } from "../artifacts/EmergencyMultisigPlugin"; import { URL_PATTERN } from "@/utils/input-values"; import { toHex } from "viem"; +import { useTransactionManager } from "@/hooks/useTransactionManager"; const UrlRegex = new RegExp(URL_PATTERN); @@ -29,47 +29,19 @@ export function useCreateProposal() { const [resources, setResources] = useState<{ name: string; url: string }[]>([ { name: PUB_APP_NAME, url: PUB_PROJECT_URL }, ]); - const { writeContract: createProposalWrite, data: createTxHash, error, status } = useWriteContract(); - const { isLoading: isConfirming, isSuccess: isConfirmed } = useWaitForTransactionReceipt({ hash: createTxHash }); const { encryptProposalData } = useEncryptedData(); - useEffect(() => { - if (status === "idle" || status === "pending") return; - else if (status === "error") { - if (error?.message?.startsWith("User rejected the request")) { - addAlert("The transaction signature was declined", { - description: "Nothing will be sent to the network", - timeout: 4 * 1000, - }); - } else { - console.error(error); - addAlert("Could not create the proposal", { type: "error" }); - } - setIsCreating(false); - return; - } - - // success - if (!createTxHash) return; - else if (isConfirming) { - addAlert("Proposal submitted", { - description: "Waiting for the transaction to be validated", - txHash: createTxHash, - }); - return; - } else if (!isConfirmed) return; - - addAlert("Proposal created", { - description: "The transaction has been validated", - type: "success", - txHash: createTxHash, - }); - - setTimeout(() => { - push("#/"); - window.scroll(0, 0); - }, 1000 * 2); - }, [status, createTxHash, isConfirming, isConfirmed]); + const { writeContract: createProposalWrite, isConfirming } = useTransactionManager({ + onSuccessMessage: "Proposal created", + onSuccess() { + setTimeout(() => { + push("#/"); + window.scroll(0, 0); + }, 1000 * 2); + }, + onErrorMessage: "Could not create the proposal", + onError: () => setIsCreating(false), + }); const submitProposal = async () => { // Check metadata diff --git a/plugins/emergency-multisig/hooks/useProposalApprovals.ts b/plugins/emergency-multisig/hooks/useProposalApprovals.ts index 954ae695..d6c5fe53 100644 --- a/plugins/emergency-multisig/hooks/useProposalApprovals.ts +++ b/plugins/emergency-multisig/hooks/useProposalApprovals.ts @@ -1,27 +1,24 @@ import { useState, useEffect } from "react"; import { Address, getAbiItem } from "viem"; -import { PublicClient } from "viem"; -import { ApprovedEvent, ApprovedEventResponse, EmergencyProposal } from "@/plugins/emergency-multisig/utils/types"; +import { usePublicClient } from "wagmi"; +import { ApprovedEvent, ApprovedEventResponse, EmergencyProposal } from "../utils/types"; import { EmergencyMultisigPluginAbi } from "../artifacts/EmergencyMultisigPlugin"; +import { PUB_CHAIN } from "@/constants"; const event = getAbiItem({ abi: EmergencyMultisigPluginAbi, name: "Approved", }); -export function useProposalApprovals( - publicClient: PublicClient, - address: Address, - proposalId: string, - proposal: EmergencyProposal | null -) { +export function useProposalApprovals(pluginAddress: Address, proposalId: string, proposal: EmergencyProposal | null) { + const publicClient = usePublicClient({ chainId: PUB_CHAIN.id }); const [proposalLogs, setLogs] = useState([]); async function getLogs() { - if (!proposal?.parameters?.snapshotBlock) return; + if (!publicClient || !proposal?.parameters?.snapshotBlock) return; const logs: ApprovedEventResponse[] = (await publicClient.getLogs({ - address, + address: pluginAddress, event: event, args: { proposalId: BigInt(proposalId), @@ -36,7 +33,7 @@ export function useProposalApprovals( useEffect(() => { getLogs(); - }, [proposal?.parameters?.snapshotBlock]); + }, [!!publicClient, proposal?.parameters?.snapshotBlock]); return proposalLogs; } diff --git a/plugins/emergency-multisig/hooks/useProposalApprove.ts b/plugins/emergency-multisig/hooks/useProposalApprove.ts index 633c05e1..fa17a631 100644 --- a/plugins/emergency-multisig/hooks/useProposalApprove.ts +++ b/plugins/emergency-multisig/hooks/useProposalApprove.ts @@ -1,74 +1,34 @@ -import { useEffect } from "react"; -import { usePublicClient, useWaitForTransactionReceipt, useWriteContract } from "wagmi"; import { useProposal } from "./useProposal"; -import { useUserCanApprove } from "@/plugins/emergency-multisig/hooks/useUserCanApprove"; -import { EmergencyMultisigPluginAbi } from "@/plugins/emergency-multisig/artifacts/EmergencyMultisigPlugin"; -import { useAlerts, AlertContextProps } from "@/context/Alerts"; -import { PUB_CHAIN, PUB_EMERGENCY_MULTISIG_PLUGIN_ADDRESS } from "@/constants"; +import { useUserCanApprove } from "./useUserCanApprove"; +import { EmergencyMultisigPluginAbi } from "../artifacts/EmergencyMultisigPlugin"; +import { PUB_EMERGENCY_MULTISIG_PLUGIN_ADDRESS } from "@/constants"; import { useProposalApprovals } from "./useProposalApprovals"; import { useRouter } from "next/router"; +import { useTransactionManager } from "@/hooks/useTransactionManager"; export function useProposalApprove(proposalId: string) { const { push } = useRouter(); - const publicClient = usePublicClient({ chainId: PUB_CHAIN.id }); const { proposal, status: proposalFetchStatus, refetch: refetchProposal } = useProposal(proposalId, true); - const approvals = useProposalApprovals(publicClient!, PUB_EMERGENCY_MULTISIG_PLUGIN_ADDRESS, proposalId, proposal); - - const { addAlert } = useAlerts() as AlertContextProps; - const { - writeContract: approveWrite, - data: approveTxHash, - error: approveError, - status: approveStatus, - } = useWriteContract(); - const { isLoading: isConfirming, isSuccess: isConfirmed } = useWaitForTransactionReceipt({ hash: approveTxHash }); const { canApprove, refetch: refetchCanApprove } = useUserCanApprove(proposalId); - - useEffect(() => { - if (approveStatus === "idle" || approveStatus === "pending") return; - else if (approveStatus === "error") { - if (approveError?.message?.startsWith("User rejected the request")) { - addAlert("The transaction signature was declined", { - description: "Nothing will be sent to the network", - timeout: 4 * 1000, - }); - } else { - console.error(approveError); - addAlert("Could not approve the proposal", { - type: "error", - description: "Check that you were part of the multisig when the proposal was created", - }); - } - return; - } - - // success - if (!approveTxHash) return; - else if (isConfirming) { - addAlert("Approval submitted", { - description: "Waiting for the transaction to be validated", - txHash: approveTxHash, - }); - return; - } else if (!isConfirmed) return; - - addAlert("Approval registered", { - description: "The transaction has been validated", - type: "success", - txHash: approveTxHash, - }); - - setTimeout(() => { - push("#/"); - window.scroll(0, 0); - }, 1000 * 2); - refetchCanApprove(); - refetchProposal(); - }, [approveStatus, approveTxHash, isConfirming, isConfirmed]); + const approvals = useProposalApprovals(PUB_EMERGENCY_MULTISIG_PLUGIN_ADDRESS, proposalId, proposal); + + const { writeContract, status, isConfirming, isConfirmed } = useTransactionManager({ + onSuccessMessage: "Approval registered", + onSuccess() { + setTimeout(() => { + push("#/"); + window.scroll(0, 0); + }, 1000 * 2); + refetchCanApprove(); + refetchProposal(); + }, + onErrorMessage: "Could not approve the proposal", + onErrorDescription: "Check that you were part of the multisig when the proposal was created", + }); const approveProposal = () => { - approveWrite({ + writeContract({ abi: EmergencyMultisigPluginAbi, address: PUB_EMERGENCY_MULTISIG_PLUGIN_ADDRESS, functionName: "approve", @@ -81,7 +41,7 @@ export function useProposalApprove(proposalId: string) { proposalFetchStatus, approvals, canApprove: !!canApprove, - isConfirming: approveStatus === "pending" || isConfirming, + isConfirming: status === "pending" || isConfirming, isConfirmed, approveProposal, }; diff --git a/plugins/emergency-multisig/hooks/useProposalExecute.ts b/plugins/emergency-multisig/hooks/useProposalExecute.ts index 5eb8b4c0..9480baba 100644 --- a/plugins/emergency-multisig/hooks/useProposalExecute.ts +++ b/plugins/emergency-multisig/hooks/useProposalExecute.ts @@ -1,5 +1,5 @@ -import { useEffect, useState } from "react"; -import { useReadContract, useWaitForTransactionReceipt, useWriteContract } from "wagmi"; +import { useState } from "react"; +import { useReadContract } from "wagmi"; import { AlertContextProps, useAlerts } from "@/context/Alerts"; import { useRouter } from "next/router"; import { PUB_CHAIN, PUB_EMERGENCY_MULTISIG_PLUGIN_ADDRESS } from "@/constants"; @@ -7,6 +7,7 @@ import { EmergencyMultisigPluginAbi } from "../artifacts/EmergencyMultisigPlugin import { toHex } from "viem"; import { useProposal } from "./useProposal"; import { getContentCid, uploadToPinata } from "@/utils/ipfs"; +import { useTransactionManager } from "@/hooks/useTransactionManager"; export function useProposalExecute(proposalId: string) { const { push } = useRouter(); @@ -28,13 +29,21 @@ export function useProposalExecute(proposalId: string) { functionName: "canExecute", args: [BigInt(proposalId)], }); - const { - writeContract: executeWrite, - data: executeTxHash, - error: executingError, - status: executingStatus, - } = useWriteContract(); - const { isLoading: isConfirming, isSuccess: isConfirmed } = useWaitForTransactionReceipt({ hash: executeTxHash }); + + const { writeContract, isConfirming, isConfirmed } = useTransactionManager({ + onSuccessMessage: "Proposal executed", + onSuccess() { + setTimeout(() => { + push("#/"); + window.scroll(0, 0); + }, 1000 * 2); + }, + onErrorMessage: "Could not execute the proposal", + onErrorDescription: "The proposal may contain actions with invalid operations", + onError() { + setIsExecuting(false); + }, + }); const executeProposal = () => { let actualMetadataUri: string; @@ -55,7 +64,7 @@ export function useProposalExecute(proposalId: string) { throw new Error("The uploaded metadata URI doesn't match"); } - executeWrite({ + writeContract({ chainId: PUB_CHAIN.id, abi: EmergencyMultisigPluginAbi, address: PUB_EMERGENCY_MULTISIG_PLUGIN_ADDRESS, @@ -70,48 +79,6 @@ export function useProposalExecute(proposalId: string) { }); }; - useEffect(() => { - if (executingStatus === "idle" || executingStatus === "pending") return; - else if (executingStatus === "error") { - if (executingError?.message?.startsWith("User rejected the request")) { - addAlert("The transaction signature was declined", { - description: "Nothing will be sent to the network", - timeout: 4 * 1000, - }); - } else { - console.error(executingError); - addAlert("Could not execute the proposal", { - type: "error", - description: "The proposal may contain actions with invalid operations", - }); - } - setIsExecuting(false); - return; - } - - // success - if (!executeTxHash) return; - else if (isConfirming) { - addAlert("Transaction submitted", { - description: "Waiting for the transaction to be validated", - type: "info", - txHash: executeTxHash, - }); - return; - } else if (!isConfirmed) return; - - addAlert("Proposal executed", { - description: "The transaction has been validated", - type: "success", - txHash: executeTxHash, - }); - - setTimeout(() => { - push("#/"); - window.scroll(0, 0); - }, 1000 * 2); - }, [executingStatus, executeTxHash, isConfirming, isConfirmed]); - return { executeProposal, canExecute: diff --git a/plugins/emergency-multisig/hooks/usePublicKeyRegistry.ts b/plugins/emergency-multisig/hooks/usePublicKeyRegistry.ts index 412153af..7592dc71 100644 --- a/plugins/emergency-multisig/hooks/usePublicKeyRegistry.ts +++ b/plugins/emergency-multisig/hooks/usePublicKeyRegistry.ts @@ -1,23 +1,33 @@ -import { useEffect, useState } from "react"; +import { useState } from "react"; import { Hex } from "viem"; import { PublicKeyRegistryAbi } from "../artifacts/PublicKeyRegistry"; -import { useConfig, useWaitForTransactionReceipt, useWriteContract } from "wagmi"; +import { useConfig } from "wagmi"; import { readContract } from "@wagmi/core"; import { PUB_PUBLIC_KEY_REGISTRY_CONTRACT_ADDRESS } from "@/constants"; import { useQuery } from "@tanstack/react-query"; import { uint8ArrayToHex } from "@/utils/hex"; import { useDerivedWallet } from "../../../hooks/useDerivedWallet"; -import { useAlerts } from "@/context/Alerts"; import { debounce } from "@/utils/debounce"; +import { useTransactionManager } from "@/hooks/useTransactionManager"; export function usePublicKeyRegistry() { const config = useConfig(); - const { addAlert } = useAlerts(); const [isRegistering, setIsRegistering] = useState(false); - const { writeContract, status: registrationStatus, data: createTxHash } = useWriteContract(); - const { isLoading: isConfirming, isSuccess: isConfirmed } = useWaitForTransactionReceipt({ hash: createTxHash }); const { publicKey, requestSignature } = useDerivedWallet(); + const { writeContract, isConfirming } = useTransactionManager({ + onSuccessMessage: "Public key registered", + onSuccess() { + setTimeout(() => refetch(), 1000 * 2); + }, + onErrorMessage: "Could not register the public key", + onError() { + // Refetch the status, just in case + debounce(() => refetch(), 800); + setIsRegistering(false); + }, + }); + const { data, isLoading, error, refetch } = useQuery({ queryKey: ["public-key-registry-items-fetching", PUB_PUBLIC_KEY_REGISTRY_CONTRACT_ADDRESS], queryFn: () => { @@ -69,43 +79,6 @@ export function usePublicKeyRegistry() { } }; - useEffect(() => { - if (registrationStatus === "idle" || registrationStatus === "pending") return; - else if (registrationStatus === "error") { - // Refetch the status, just in case - debounce(() => refetch(), 800); - - if (error?.message?.startsWith("User rejected the request")) { - addAlert("The transaction signature was declined", { - description: "Nothing will be sent to the network", - timeout: 4 * 1000, - }); - } else { - console.error(error); - addAlert("Could not register the public key", { type: "error" }); - } - setIsRegistering(false); - return; - } - - // success - if (!createTxHash) return; - else if (isConfirming) { - addAlert("Transaction submitted", { - description: "Waiting for the transaction to be validated", - txHash: createTxHash, - }); - return; - } else if (!isConfirmed) return; - - addAlert("Public key registered", { - description: "The transaction has been validated", - type: "success", - txHash: createTxHash, - }); - setTimeout(() => refetch(), 1000 * 2); - }, [registrationStatus, createTxHash, isConfirming, isConfirmed]); - return { data: data || [], registerPublicKey, diff --git a/plugins/members/hooks/useAnnounceDelegation.ts b/plugins/members/hooks/useAnnounceDelegation.ts index 12dd3b64..589f8fc8 100644 --- a/plugins/members/hooks/useAnnounceDelegation.ts +++ b/plugins/members/hooks/useAnnounceDelegation.ts @@ -1,56 +1,27 @@ import { DelegateAnnouncerAbi } from "../artifacts/DelegationWall.sol"; import { PUB_DELEGATION_WALL_CONTRACT_ADDRESS } from "@/constants"; -import { useWaitForTransactionReceipt, useWriteContract } from "wagmi"; import { toHex } from "viem"; import { useAlerts } from "@/context/Alerts"; import { uploadToPinata } from "@/utils/ipfs"; -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useState } from "react"; import { type IAnnouncementMetadata } from "../utils/types"; import { useDelegates } from "./useDelegates"; +import { useTransactionManager } from "@/hooks/useTransactionManager"; export function useAnnounceDelegation(onSuccess?: () => void) { const { addAlert } = useAlerts(); - const { writeContract, data: hash, error, status } = useWriteContract(); const { refetch } = useDelegates(); const [uploading, setUploading] = useState(false); - const { isLoading: isConfirming, isSuccess: isConfirmed } = useWaitForTransactionReceipt({ hash }); - useEffect(() => { - if (status === "idle" || status === "pending") return; - else if (status === "error") { - if (error?.message?.startsWith("User rejected the request")) { - addAlert("The transaction signature was declined", { - description: "Nothing will be sent to the network", - timeout: 4 * 1000, - }); - } else { - console.error("Could not create delegate profile", error); - addAlert("Could not create your delegate profile", { type: "error" }); - } - return; - } - - // success - if (!hash) return; - else if (isConfirming) { - addAlert("Delegate profile submitted", { - description: "Waiting for the transaction to be validated", - txHash: hash, - }); - return; - } else if (!isConfirmed) return; - - addAlert("Delegate profile registered", { - description: "The transaction has been validated", - type: "success", - txHash: hash, - }); - - // Force a refresh of the delegates list - refetch(); - - onSuccess?.(); - }, [status, hash, isConfirming, isConfirmed]); + const { writeContract, status, isConfirming, isConfirmed } = useTransactionManager({ + onSuccessMessage: "Delegate profile registered", + onSuccess() { + // Force a refresh of the delegates list + refetch(); + onSuccess?.(); + }, + onErrorMessage: "Could not create your delegate profile", + }); const announceDelegation = useCallback( async (metadata: IAnnouncementMetadata) => { diff --git a/plugins/multisig/hooks/useProposalApprovals.ts b/plugins/multisig/hooks/useProposalApprovals.ts index 2a473865..83a97d23 100644 --- a/plugins/multisig/hooks/useProposalApprovals.ts +++ b/plugins/multisig/hooks/useProposalApprovals.ts @@ -1,31 +1,28 @@ import { useState, useEffect } from "react"; import { Address, getAbiItem } from "viem"; -import { PublicClient } from "viem"; -import { MultisigProposal, ApprovedEvent, ApprovedEventResponse } from "@/plugins/multisig/utils/types"; +import { usePublicClient } from "wagmi"; +import { MultisigProposal, ApprovedEvent, ApprovedEventResponse } from "../utils/types"; import { MultisigPluginAbi } from "../artifacts/MultisigPlugin"; +import { PUB_CHAIN } from "@/constants"; const event = getAbiItem({ abi: MultisigPluginAbi, name: "Approved", }); -export function useProposalApprovals( - publicClient: PublicClient, - address: Address, - proposalId: string, - proposal: MultisigProposal | null -) { +export function useProposalApprovals(pluginAddress: Address, proposalId: string, proposal: MultisigProposal | null) { + const publicClient = usePublicClient({ chainId: PUB_CHAIN.id }); const [proposalLogs, setLogs] = useState([]); async function getLogs() { - if (!proposal?.parameters?.snapshotBlock) return; + if (!publicClient || !proposal?.parameters?.snapshotBlock) return; const logs: ApprovedEventResponse[] = (await publicClient.getLogs({ - address, - event: event as any, + address: pluginAddress, + event: event, args: { - proposalId, - } as any, + proposalId: BigInt(proposalId), + }, fromBlock: proposal.parameters.snapshotBlock, toBlock: "latest", // TODO: Make this variable between 'latest' and proposal last block })) as any; @@ -36,7 +33,7 @@ export function useProposalApprovals( useEffect(() => { getLogs(); - }, [proposal?.parameters?.snapshotBlock]); + }, [!!publicClient, proposal?.parameters?.snapshotBlock]); return proposalLogs; } diff --git a/plugins/multisig/hooks/useProposalApprove.ts b/plugins/multisig/hooks/useProposalApprove.ts index 3536b70f..6df6e750 100644 --- a/plugins/multisig/hooks/useProposalApprove.ts +++ b/plugins/multisig/hooks/useProposalApprove.ts @@ -1,75 +1,34 @@ -import { useEffect } from "react"; -import { usePublicClient, useWaitForTransactionReceipt, useWriteContract } from "wagmi"; import { useProposal } from "./useProposal"; -import { useUserCanApprove } from "@/plugins/multisig/hooks/useUserCanApprove"; -import { MultisigPluginAbi } from "@/plugins/multisig/artifacts/MultisigPlugin"; -import { useAlerts, AlertContextProps } from "@/context/Alerts"; -import { PUB_CHAIN, PUB_MULTISIG_PLUGIN_ADDRESS } from "@/constants"; +import { useUserCanApprove } from "./useUserCanApprove"; +import { MultisigPluginAbi } from "../artifacts/MultisigPlugin"; +import { PUB_MULTISIG_PLUGIN_ADDRESS } from "@/constants"; import { useProposalApprovals } from "./useProposalApprovals"; import { useRouter } from "next/router"; +import { useTransactionManager } from "@/hooks/useTransactionManager"; export function useProposalApprove(proposalId: string) { const { push } = useRouter(); - const publicClient = usePublicClient({ chainId: PUB_CHAIN.id }); const { proposal, status: proposalFetchStatus, refetch: refetchProposal } = useProposal(proposalId, true); - const approvals = useProposalApprovals(publicClient!, PUB_MULTISIG_PLUGIN_ADDRESS, proposalId, proposal); - - const { addAlert } = useAlerts() as AlertContextProps; - const { - writeContract: approveWrite, - data: approveTxHash, - error: approveError, - status: approveStatus, - } = useWriteContract(); - const { isLoading: isConfirming, isSuccess: isConfirmed } = useWaitForTransactionReceipt({ hash: approveTxHash }); const { canApprove, refetch: refetchCanApprove } = useUserCanApprove(proposalId); - - useEffect(() => { - if (approveStatus === "idle" || approveStatus === "pending") return; - else if (approveStatus === "error") { - if (approveError?.message?.startsWith("User rejected the request")) { - addAlert("The transaction signature was declined", { - description: "Nothing will be sent to the network", - timeout: 4 * 1000, - }); - } else { - console.error(approveError); - addAlert("Could not approve the proposal", { - type: "error", - description: - "Check that you were part of the multisig when the proposal was created and that the proposal doesn't contain actions that could revert", - }); - } - return; - } - - // success - if (!approveTxHash) return; - else if (isConfirming) { - addAlert("Approval submitted", { - description: "Waiting for the transaction to be validated", - txHash: approveTxHash, - }); - return; - } else if (!isConfirmed) return; - - addAlert("Approval registered", { - description: "The transaction has been validated", - type: "success", - txHash: approveTxHash, - }); - - setTimeout(() => { - push("#/"); - window.scroll(0, 0); - }, 1000 * 2); - refetchCanApprove(); - refetchProposal(); - }, [approveStatus, approveTxHash, isConfirming, isConfirmed]); + const approvals = useProposalApprovals(PUB_MULTISIG_PLUGIN_ADDRESS, proposalId, proposal); + + const { writeContract, status, isConfirming, isConfirmed } = useTransactionManager({ + onSuccessMessage: "Approval registered", + onSuccess() { + setTimeout(() => { + push("#/"); + window.scroll(0, 0); + }, 1000 * 2); + refetchCanApprove(); + refetchProposal(); + }, + onErrorMessage: "Could not approve the proposal", + onErrorDescription: "Check that you were part of the multisig when the proposal was created", + }); const approveProposal = () => { - approveWrite({ + writeContract({ abi: MultisigPluginAbi, address: PUB_MULTISIG_PLUGIN_ADDRESS, functionName: "approve", @@ -82,7 +41,7 @@ export function useProposalApprove(proposalId: string) { proposalFetchStatus, approvals, canApprove: !!canApprove, - isConfirming: approveStatus === "pending" || isConfirming, + isConfirming: status === "pending" || isConfirming, isConfirmed, approveProposal, }; From 99a7885b691b5b482e8fd0ec3179d981eefea58f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8r=E2=88=82=C2=A1?= Date: Thu, 29 Aug 2024 17:08:57 +0200 Subject: [PATCH 2/2] Encapsulating all remaining transactions into useTransactionManager --- plugins/lockToVote/hooks/useCreateProposal.ts | 53 ++++--------- .../lockToVote/hooks/useProposalClaimLock.ts | 76 +++++-------------- .../lockToVote/hooks/useProposalExecute.ts | 67 ++++------------ plugins/lockToVote/hooks/useProposalVeto.ts | 70 ++++++----------- plugins/lockToVote/hooks/useProposalVetoes.ts | 17 ++--- .../members/hooks/useDelegateVotingPower.ts | 61 ++++----------- plugins/multisig/hooks/useCreateProposal.ts | 50 +++--------- plugins/multisig/hooks/useProposalExecute.ts | 73 +++++------------- .../hooks/useProposalExecute.ts | 65 ++++------------ .../hooks/useProposalVeto.ts | 56 ++++---------- .../tokenVoting/hooks/useCreateProposal.ts | 53 ++++--------- .../tokenVoting/hooks/useProposalExecute.ts | 71 +++++------------ .../tokenVoting/hooks/useProposalVoting.ts | 52 +++---------- 13 files changed, 203 insertions(+), 561 deletions(-) diff --git a/plugins/lockToVote/hooks/useCreateProposal.ts b/plugins/lockToVote/hooks/useCreateProposal.ts index d26edae9..899bb7f8 100644 --- a/plugins/lockToVote/hooks/useCreateProposal.ts +++ b/plugins/lockToVote/hooks/useCreateProposal.ts @@ -1,13 +1,13 @@ import { useRouter } from "next/router"; -import { useEffect, useState } from "react"; +import { useState } from "react"; import { ProposalMetadata, RawAction } from "@/utils/types"; -import { useWaitForTransactionReceipt, useWriteContract } from "wagmi"; import { useAlerts } from "@/context/Alerts"; import { PUB_APP_NAME, PUB_CHAIN, PUB_LOCK_TO_VOTE_PLUGIN_ADDRESS, PUB_PROJECT_URL } from "@/constants"; import { uploadToPinata } from "@/utils/ipfs"; import { LockToVetoPluginAbi } from "../artifacts/LockToVetoPlugin.sol"; import { URL_PATTERN } from "@/utils/input-values"; import { toHex } from "viem"; +import { useTransactionManager } from "@/hooks/useTransactionManager"; const UrlRegex = new RegExp(URL_PATTERN); @@ -22,45 +22,18 @@ export function useCreateProposal() { const [resources, setResources] = useState<{ name: string; url: string }[]>([ { name: PUB_APP_NAME, url: PUB_PROJECT_URL }, ]); - const { writeContract: createProposalWrite, data: createTxHash, error, status } = useWriteContract(); - const { isLoading: isConfirming, isSuccess: isConfirmed } = useWaitForTransactionReceipt({ hash: createTxHash }); - useEffect(() => { - if (status === "idle" || status === "pending") return; - else if (status === "error") { - if (error?.message?.startsWith("User rejected the request")) { - addAlert("The transaction signature was declined", { - description: "Nothing will be sent to the network", - timeout: 4 * 1000, - }); - } else { - console.error(error); - addAlert("Could not create the proposal", { type: "error" }); - } - setIsCreating(false); - return; - } - - // success - if (!createTxHash) return; - else if (isConfirming) { - addAlert("Proposal submitted", { - description: "Waiting for the transaction to be validated", - txHash: createTxHash, - }); - return; - } else if (!isConfirmed) return; - - addAlert("Proposal created", { - description: "The transaction has been validated", - type: "success", - txHash: createTxHash, - }); - setTimeout(() => { - push("#/"); - window.scroll(0, 0); - }, 1000 * 2); - }, [status, createTxHash, isConfirming, isConfirmed]); + const { writeContract: createProposalWrite, isConfirming } = useTransactionManager({ + onSuccessMessage: "Proposal created", + onSuccess() { + setTimeout(() => { + push("#/"); + window.scroll(0, 0); + }, 1000 * 2); + }, + onErrorMessage: "Could not create the proposal", + onError: () => setIsCreating(false), + }); const submitProposal = async () => { // Check metadata diff --git a/plugins/lockToVote/hooks/useProposalClaimLock.ts b/plugins/lockToVote/hooks/useProposalClaimLock.ts index a5572021..02958e77 100644 --- a/plugins/lockToVote/hooks/useProposalClaimLock.ts +++ b/plugins/lockToVote/hooks/useProposalClaimLock.ts @@ -1,20 +1,16 @@ -import { useEffect } from "react"; import { useAccount, useReadContract, useWaitForTransactionReceipt, useWriteContract } from "wagmi"; -import { AlertContextProps, useAlerts } from "@/context/Alerts"; import { useRouter } from "next/router"; import { PUB_CHAIN, PUB_LOCK_TO_VOTE_PLUGIN_ADDRESS } from "@/constants"; import { LockToVetoPluginAbi } from "../artifacts/LockToVetoPlugin.sol"; +import { useTransactionManager } from "@/hooks/useTransactionManager"; +import { useState } from "react"; export function useProposalClaimLock(proposalIdx: number) { const { reload } = useRouter(); const account = useAccount(); - const { addAlert } = useAlerts() as AlertContextProps; + const [isClaiming, setIsClaiming] = useState(false); - const { - data: hasClaimed, - isError: isCanVoteError, - isLoading: isCanVoteLoading, - } = useReadContract({ + const { data: hasClaimed } = useReadContract({ address: PUB_LOCK_TO_VOTE_PLUGIN_ADDRESS, abi: LockToVetoPluginAbi, chainId: PUB_CHAIN.id, @@ -24,19 +20,26 @@ export function useProposalClaimLock(proposalIdx: number) { enabled: !!account.address, }, }); - const { - writeContract: claimLockWrite, - data: executeTxHash, - error: executingError, - status: claimingStatus, - } = useWriteContract(); - const { isLoading: isConfirming, isSuccess: isConfirmed } = useWaitForTransactionReceipt({ hash: executeTxHash }); + + const { writeContract, isConfirming, isConfirmed } = useTransactionManager({ + onSuccessMessage: "Claim executed", + onSuccess() { + reload(); + setIsClaiming(false); + }, + onErrorMessage: "Could not claim the locked tokens", + onErrorDescription: "Please get in touch with us", + onError() { + setIsClaiming(false); + }, + }); const claimLockProposal = () => { if (hasClaimed) return; - console.log(proposalIdx, account.address); - claimLockWrite({ + setIsClaiming(true); + + writeContract({ chainId: PUB_CHAIN.id, abi: LockToVetoPluginAbi, address: PUB_LOCK_TO_VOTE_PLUGIN_ADDRESS, @@ -45,47 +48,10 @@ export function useProposalClaimLock(proposalIdx: number) { }); }; - useEffect(() => { - if (claimingStatus === "idle" || claimingStatus === "pending") return; - else if (claimingStatus === "error") { - if (executingError?.message?.startsWith("User rejected the request")) { - addAlert("Transaction rejected by the user", { - timeout: 4 * 1000, - }); - } else { - console.error(executingError); - addAlert("Could not claim locked tokens", { - type: "error", - description: "The proposal may contain actions with invalid operations. Please get in contact with us.", - }); - } - return; - } - - // success - if (!executeTxHash) return; - else if (isConfirming) { - addAlert("Claim submitted", { - description: "Waiting for the transaction to be validated", - type: "info", - txHash: executeTxHash, - }); - return; - } else if (!isConfirmed) return; - - addAlert("Claim executed", { - description: "The transaction has been validated", - type: "success", - txHash: executeTxHash, - }); - - setTimeout(() => reload(), 1000 * 2); - }, [claimingStatus, executeTxHash, isConfirming, isConfirmed]); - return { claimLockProposal, hasClaimed: !!hasClaimed, - isConfirming, + isConfirming: isConfirming || isClaiming, isConfirmed, }; } diff --git a/plugins/lockToVote/hooks/useProposalExecute.ts b/plugins/lockToVote/hooks/useProposalExecute.ts index e19d9aed..a191a2b1 100644 --- a/plugins/lockToVote/hooks/useProposalExecute.ts +++ b/plugins/lockToVote/hooks/useProposalExecute.ts @@ -1,13 +1,12 @@ -import { useEffect, useState } from "react"; -import { useReadContract, useWaitForTransactionReceipt, useWriteContract } from "wagmi"; -import { AlertContextProps, useAlerts } from "@/context/Alerts"; +import { useState } from "react"; +import { useReadContract } from "wagmi"; import { useRouter } from "next/router"; import { PUB_CHAIN, PUB_LOCK_TO_VOTE_PLUGIN_ADDRESS } from "@/constants"; import { LockToVetoPluginAbi } from "../artifacts/LockToVetoPlugin.sol"; +import { useTransactionManager } from "@/hooks/useTransactionManager"; export function useProposalExecute(proposalIdx: number) { const { reload } = useRouter(); - const { addAlert } = useAlerts() as AlertContextProps; const [isExecuting, setIsExecuting] = useState(false); const { @@ -21,13 +20,18 @@ export function useProposalExecute(proposalIdx: number) { functionName: "canExecute", args: [BigInt(proposalIdx)], }); - const { - writeContract: executeWrite, - data: executeTxHash, - error: executingError, - status: executingStatus, - } = useWriteContract(); - const { isLoading: isConfirming, isSuccess: isConfirmed } = useWaitForTransactionReceipt({ hash: executeTxHash }); + + const { writeContract, isConfirming, isConfirmed } = useTransactionManager({ + onSuccessMessage: "Proposal executed", + onSuccess() { + setTimeout(() => reload(), 1000 * 2); + }, + onErrorMessage: "Could not execute the proposal", + onErrorDescription: "The proposal may contain actions with invalid operations", + onError() { + setIsExecuting(false); + }, + }); const executeProposal = () => { if (!canExecute) return; @@ -35,7 +39,7 @@ export function useProposalExecute(proposalIdx: number) { setIsExecuting(true); - executeWrite({ + writeContract({ chainId: PUB_CHAIN.id, abi: LockToVetoPluginAbi, address: PUB_LOCK_TO_VOTE_PLUGIN_ADDRESS, @@ -44,45 +48,6 @@ export function useProposalExecute(proposalIdx: number) { }); }; - useEffect(() => { - if (executingStatus === "idle" || executingStatus === "pending") return; - else if (executingStatus === "error") { - if (executingError?.message?.startsWith("User rejected the request")) { - addAlert("The transaction signature was declined", { - description: "Nothing will be sent to the network", - timeout: 4 * 1000, - }); - } else { - console.error(executingError); - addAlert("Could not execute the proposal", { - type: "error", - description: "The proposal may contain actions with invalid operations", - }); - } - setIsExecuting(false); - return; - } - - // success - if (!executeTxHash) return; - else if (isConfirming) { - addAlert("Transaction submitted", { - description: "Waiting for the transaction to be validated", - type: "info", - txHash: executeTxHash, - }); - return; - } else if (!isConfirmed) return; - - addAlert("Proposal executed", { - description: "The transaction has been validated", - type: "success", - txHash: executeTxHash, - }); - - setTimeout(() => reload(), 1000 * 2); - }, [executingStatus, executeTxHash, isConfirming, isConfirmed]); - return { executeProposal, canExecute: !isCanVoteError && !isCanVoteLoading && !isConfirmed && !!canExecute, diff --git a/plugins/lockToVote/hooks/useProposalVeto.ts b/plugins/lockToVote/hooks/useProposalVeto.ts index 6860c8ea..8124ce97 100644 --- a/plugins/lockToVote/hooks/useProposalVeto.ts +++ b/plugins/lockToVote/hooks/useProposalVeto.ts @@ -1,5 +1,4 @@ -import { useEffect } from "react"; -import { usePublicClient, useWaitForTransactionReceipt, useWriteContract, useReadContract, useAccount } from "wagmi"; +import { useReadContract, useAccount } from "wagmi"; import { Address } from "viem"; import { ERC20PermitAbi } from "@/artifacts/ERC20Permit.sol"; import { useProposal } from "./useProposal"; @@ -7,63 +6,38 @@ import { useProposalVetoes } from "./useProposalVetoes"; import { useUserCanVeto } from "./useUserCanVeto"; import { LockToVetoPluginAbi } from "../artifacts/LockToVetoPlugin.sol"; import { usePermit } from "@/hooks/usePermit"; -import { useAlerts, AlertContextProps } from "@/context/Alerts"; -import { PUB_CHAIN, PUB_TOKEN_ADDRESS, PUB_LOCK_TO_VOTE_PLUGIN_ADDRESS } from "@/constants"; +import { PUB_TOKEN_ADDRESS, PUB_LOCK_TO_VOTE_PLUGIN_ADDRESS } from "@/constants"; +import { useTransactionManager } from "@/hooks/useTransactionManager"; +import { ADDRESS_ZERO } from "@/utils/evm"; export function useProposalVeto(proposalId: number) { - const publicClient = usePublicClient({ chainId: PUB_CHAIN.id }); - const { proposal, status: proposalFetchStatus, refetch: refetchProposal } = useProposal(proposalId, true); - const vetoes = useProposalVetoes(publicClient!, PUB_LOCK_TO_VOTE_PLUGIN_ADDRESS, proposalId, proposal); + const vetoes = useProposalVetoes(PUB_LOCK_TO_VOTE_PLUGIN_ADDRESS, proposalId, proposal); const { signPermit, refetchPermitData } = usePermit(); - - const { addAlert } = useAlerts() as AlertContextProps; - const account_address = useAccount().address!; + const { address } = useAccount(); const { data: balanceData } = useReadContract({ address: PUB_TOKEN_ADDRESS, abi: ERC20PermitAbi, functionName: "balanceOf", - args: [account_address], + args: [address || ADDRESS_ZERO], }); - - const { writeContract: vetoWrite, data: vetoTxHash, error: vetoingError, status: vetoingStatus } = useWriteContract(); - const { isLoading: isConfirming, isSuccess: isConfirmed } = useWaitForTransactionReceipt({ hash: vetoTxHash }); const { canVeto, refetch: refetchCanVeto } = useUserCanVeto(proposalId); - useEffect(() => { - if (vetoingStatus === "idle" || vetoingStatus === "pending") return; - else if (vetoingStatus === "error") { - if (vetoingError?.message?.startsWith("User rejected the request")) { - addAlert("Transaction rejected by the user", { - timeout: 4 * 1000, - }); - } else { - console.error(vetoingError); - addAlert("Could not create the veto", { type: "error" }); - } - return; - } - - // success - if (!vetoTxHash) return; - else if (isConfirming) { - addAlert("Veto submitted", { - description: "Waiting for the transaction to be validated", - txHash: vetoTxHash, - }); - return; - } else if (!isConfirmed) return; - - addAlert("Veto registered", { - description: "The transaction has been validated", - type: "success", - txHash: vetoTxHash, - }); - refetchCanVeto(); - refetchProposal(); - refetchPermitData(); - }, [vetoingStatus, vetoTxHash, isConfirming, isConfirmed]); + const { + writeContract, + status: vetoingStatus, + isConfirming, + isConfirmed, + } = useTransactionManager({ + onSuccessMessage: "Veto registered", + onSuccess() { + refetchCanVeto(); + refetchProposal(); + refetchPermitData(); + }, + onErrorMessage: "Could not submit the veto", + }); const vetoProposal = () => { const dest: Address = PUB_LOCK_TO_VOTE_PLUGIN_ADDRESS; @@ -73,7 +47,7 @@ export function useProposalVeto(proposalId: number) { signPermit(dest, value, deadline).then((sig) => { if (!sig?.yParity) throw new Error("Invalid signature"); - vetoWrite({ + writeContract({ abi: LockToVetoPluginAbi, address: dest, functionName: "vetoPermit", diff --git a/plugins/lockToVote/hooks/useProposalVetoes.ts b/plugins/lockToVote/hooks/useProposalVetoes.ts index e0575672..2d4981ae 100644 --- a/plugins/lockToVote/hooks/useProposalVetoes.ts +++ b/plugins/lockToVote/hooks/useProposalVetoes.ts @@ -1,27 +1,24 @@ import { useState, useEffect } from "react"; import { Address, getAbiItem } from "viem"; -import { PublicClient } from "viem"; import { LockToVetoPluginAbi } from "../artifacts/LockToVetoPlugin.sol"; import { Proposal, VetoCastEvent, VoteCastResponse } from "../utils/types"; +import { usePublicClient } from "wagmi"; +import { PUB_CHAIN } from "@/constants"; const event = getAbiItem({ abi: LockToVetoPluginAbi, name: "VetoCast", }); -export function useProposalVetoes( - publicClient: PublicClient, - address: Address, - proposalId: number, - proposal: Proposal | null -) { +export function useProposalVetoes(pluginAddress: Address, proposalId: number, proposal: Proposal | null) { + const publicClient = usePublicClient({ chainId: PUB_CHAIN.id }); const [proposalLogs, setLogs] = useState([]); async function getLogs() { - if (!proposal?.parameters?.snapshotBlock) return; + if (!publicClient || !proposal?.parameters?.snapshotBlock) return; const logs: VoteCastResponse[] = (await publicClient.getLogs({ - address, + address: pluginAddress, event, args: { proposalId: BigInt(proposalId), @@ -36,7 +33,7 @@ export function useProposalVetoes( useEffect(() => { getLogs(); - }, [proposal?.parameters?.snapshotBlock]); + }, [!!publicClient, proposal?.parameters?.snapshotBlock]); return proposalLogs; } diff --git a/plugins/members/hooks/useDelegateVotingPower.ts b/plugins/members/hooks/useDelegateVotingPower.ts index 8472e566..38a8aca1 100644 --- a/plugins/members/hooks/useDelegateVotingPower.ts +++ b/plugins/members/hooks/useDelegateVotingPower.ts @@ -1,54 +1,25 @@ import { iVotesAbi } from "../artifacts/iVotes.sol"; import { PUB_TOKEN_ADDRESS } from "@/constants"; -import { useAlerts } from "@/context/Alerts"; -import { useEffect, useState } from "react"; +import { useTransactionManager } from "@/hooks/useTransactionManager"; +import { useState } from "react"; import { type Address } from "viem"; -import { useWaitForTransactionReceipt, useWriteContract } from "wagmi"; export const useDelegateVotingPower = (targetAddress: Address, onSuccess?: () => void) => { - const { addAlert } = useAlerts(); - const [isConfirming, setIsConfirming] = useState(false); - const { writeContract, data: hash, error, status } = useWriteContract(); - const { isLoading, isSuccess: isConfirmed } = useWaitForTransactionReceipt({ hash }); - - useEffect(() => { - if (status === "idle" || status === "pending") return; - else if (status === "error") { - if (error?.message?.startsWith("User rejected the request")) { - addAlert("The transaction signature was declined", { - description: "Nothing will be sent to the network", - timeout: 4 * 1000, - }); - } else { - console.error(`Could not delegate`, error); - addAlert(`Could not delegate`, { type: "error" }); - } - setIsConfirming(false); - return; - } - - // success - if (!hash) return; - else if (isLoading) { - addAlert("Delegation submitted", { - description: "Waiting for the transaction to be validated", - txHash: hash, - }); - setIsConfirming(false); - return; - } else if (!isConfirmed) return; - - addAlert("Delegation registered", { - description: "The transaction has been validated", - type: "success", - txHash: hash, - }); - - if (typeof onSuccess === "function") onSuccess(); - }, [status, hash, isLoading, isConfirmed]); + const [isDelegating, setIsDelegating] = useState(false); + + const { writeContract, status, isConfirming, isConfirmed } = useTransactionManager({ + onSuccessMessage: "Delegation registered", + onSuccess() { + if (typeof onSuccess === "function") onSuccess(); + }, + onErrorMessage: "Could not delegate", + onError() { + setIsDelegating(false); + }, + }); const delegateVotingPower = () => { - setIsConfirming(true); + setIsDelegating(true); writeContract({ abi: iVotesAbi, @@ -61,7 +32,7 @@ export const useDelegateVotingPower = (targetAddress: Address, onSuccess?: () => return { delegateVotingPower, isConfirmed, - isLoading: isConfirming || isLoading, + isLoading: isDelegating || isConfirming, status, }; }; diff --git a/plugins/multisig/hooks/useCreateProposal.ts b/plugins/multisig/hooks/useCreateProposal.ts index 48a1f8f9..e4ee264e 100644 --- a/plugins/multisig/hooks/useCreateProposal.ts +++ b/plugins/multisig/hooks/useCreateProposal.ts @@ -14,6 +14,7 @@ import { uploadToPinata } from "@/utils/ipfs"; import { MultisigPluginAbi } from "../artifacts/MultisigPlugin"; import { URL_PATTERN } from "@/utils/input-values"; import { toHex } from "viem"; +import { useTransactionManager } from "@/hooks/useTransactionManager"; const UrlRegex = new RegExp(URL_PATTERN); @@ -28,45 +29,18 @@ export function useCreateProposal() { const [resources, setResources] = useState<{ name: string; url: string }[]>([ { name: PUB_APP_NAME, url: PUB_PROJECT_URL }, ]); - const { writeContract: createProposalWrite, data: createTxHash, error, status } = useWriteContract(); - const { isLoading: isConfirming, isSuccess: isConfirmed } = useWaitForTransactionReceipt({ hash: createTxHash }); - useEffect(() => { - if (status === "idle" || status === "pending") return; - else if (status === "error") { - if (error?.message?.startsWith("User rejected the request")) { - addAlert("The transaction signature was declined", { - description: "Nothing will be sent to the network", - timeout: 4 * 1000, - }); - } else { - console.error(error); - addAlert("Could not create the proposal", { type: "error" }); - } - setIsCreating(false); - return; - } - - // success - if (!createTxHash) return; - else if (isConfirming) { - addAlert("Proposal submitted", { - description: "Waiting for the transaction to be validated", - txHash: createTxHash, - }); - return; - } else if (!isConfirmed) return; - - addAlert("Proposal created", { - description: "The transaction has been validated", - type: "success", - txHash: createTxHash, - }); - setTimeout(() => { - push("#/"); - window.scroll(0, 0); - }, 1000 * 2); - }, [status, createTxHash, isConfirming, isConfirmed]); + const { writeContract: createProposalWrite, isConfirming } = useTransactionManager({ + onSuccessMessage: "Proposal created", + onSuccess() { + setTimeout(() => { + push("#/"); + window.scroll(0, 0); + }, 1000 * 2); + }, + onErrorMessage: "Could not create the proposal", + onError: () => setIsCreating(false), + }); const submitProposal = async () => { // Check metadata diff --git a/plugins/multisig/hooks/useProposalExecute.ts b/plugins/multisig/hooks/useProposalExecute.ts index 9fef98d0..0c9da3c6 100644 --- a/plugins/multisig/hooks/useProposalExecute.ts +++ b/plugins/multisig/hooks/useProposalExecute.ts @@ -1,14 +1,13 @@ -import { useEffect, useState } from "react"; -import { useReadContract, useWaitForTransactionReceipt, useWriteContract } from "wagmi"; -import { AlertContextProps, useAlerts } from "@/context/Alerts"; +import { useState } from "react"; +import { useReadContract } from "wagmi"; import { useRouter } from "next/router"; import { PUB_CHAIN, PUB_MULTISIG_PLUGIN_ADDRESS } from "@/constants"; import { MultisigPluginAbi } from "../artifacts/MultisigPlugin"; +import { useTransactionManager } from "@/hooks/useTransactionManager"; export function useProposalExecute(proposalId: string) { const { push } = useRouter(); const [isExecuting, setIsExecuting] = useState(false); - const { addAlert } = useAlerts() as AlertContextProps; const { data: canExecute, @@ -21,20 +20,28 @@ export function useProposalExecute(proposalId: string) { functionName: "canExecute", args: [BigInt(proposalId)], }); - const { - writeContract: executeWrite, - data: executeTxHash, - error: executingError, - status: executingStatus, - } = useWriteContract(); - const { isLoading: isConfirming, isSuccess: isConfirmed } = useWaitForTransactionReceipt({ hash: executeTxHash }); + + const { writeContract, isConfirming, isConfirmed } = useTransactionManager({ + onSuccessMessage: "Proposal executed", + onSuccess() { + setTimeout(() => { + push("#/"); + window.scroll(0, 0); + }, 1000 * 2); + }, + onErrorMessage: "Could not execute the proposal", + onErrorDescription: "The proposal may contain actions with invalid operations", + onError() { + setIsExecuting(false); + }, + }); const executeProposal = () => { if (!canExecute) return; setIsExecuting(true); - executeWrite({ + writeContract({ chainId: PUB_CHAIN.id, abi: MultisigPluginAbi, address: PUB_MULTISIG_PLUGIN_ADDRESS, @@ -43,48 +50,6 @@ export function useProposalExecute(proposalId: string) { }); }; - useEffect(() => { - if (executingStatus === "idle" || executingStatus === "pending") return; - else if (executingStatus === "error") { - if (executingError?.message?.startsWith("User rejected the request")) { - addAlert("The transaction signature was declined", { - description: "Nothing will be sent to the network", - timeout: 4 * 1000, - }); - } else { - console.error(executingError); - addAlert("Could not execute the proposal", { - type: "error", - description: "The proposal may contain actions with invalid operations", - }); - } - setIsExecuting(false); - return; - } - - // success - if (!executeTxHash) return; - else if (isConfirming) { - addAlert("Transaction submitted", { - description: "Waiting for the transaction to be validated", - type: "info", - txHash: executeTxHash, - }); - return; - } else if (!isConfirmed) return; - - addAlert("Proposal executed", { - description: "The transaction has been validated", - type: "success", - txHash: executeTxHash, - }); - - setTimeout(() => { - push("#/"); - window.scroll(0, 0); - }, 1000 * 2); - }, [executingStatus, executeTxHash, isConfirming, isConfirmed]); - return { executeProposal, canExecute: !isCanVoteError && !isCanVoteLoading && !isConfirmed && !!canExecute, diff --git a/plugins/optimistic-proposals/hooks/useProposalExecute.ts b/plugins/optimistic-proposals/hooks/useProposalExecute.ts index 782fe451..739982ae 100644 --- a/plugins/optimistic-proposals/hooks/useProposalExecute.ts +++ b/plugins/optimistic-proposals/hooks/useProposalExecute.ts @@ -1,10 +1,11 @@ -import { useEffect, useState } from "react"; -import { useReadContract, useWaitForTransactionReceipt, useWriteContract } from "wagmi"; +import { useState } from "react"; +import { useReadContract } from "wagmi"; import { OptimisticTokenVotingPluginAbi } from "../artifacts/OptimisticTokenVotingPlugin.sol"; import { AlertContextProps, useAlerts } from "@/context/Alerts"; import { useRouter } from "next/router"; import { PUB_CHAIN, PUB_DUAL_GOVERNANCE_PLUGIN_ADDRESS } from "@/constants"; import { useProposalId } from "./useProposalId"; +import { useTransactionManager } from "@/hooks/useTransactionManager"; export function useProposalExecute(index: number) { const { reload } = useRouter(); @@ -23,13 +24,18 @@ export function useProposalExecute(index: number) { functionName: "canExecute", args: [proposalId ?? BigInt("0")], }); - const { - writeContract: executeWrite, - data: executeTxHash, - error: executingError, - status: executingStatus, - } = useWriteContract(); - const { isLoading: isConfirming, isSuccess: isConfirmed } = useWaitForTransactionReceipt({ hash: executeTxHash }); + + const { writeContract, isConfirming, isConfirmed } = useTransactionManager({ + onSuccessMessage: "Proposal executed", + onSuccess() { + setTimeout(() => reload(), 1000 * 2); + }, + onErrorMessage: "Could not execute the proposal", + onErrorDescription: "The proposal may contain actions with invalid operations", + onError() { + setIsExecuting(false); + }, + }); const executeProposal = () => { if (!canExecute) return; @@ -37,7 +43,7 @@ export function useProposalExecute(index: number) { setIsExecuting(true); - executeWrite({ + writeContract({ chainId: PUB_CHAIN.id, abi: OptimisticTokenVotingPluginAbi, address: PUB_DUAL_GOVERNANCE_PLUGIN_ADDRESS, @@ -46,45 +52,6 @@ export function useProposalExecute(index: number) { }); }; - useEffect(() => { - if (executingStatus === "idle" || executingStatus === "pending") return; - else if (executingStatus === "error") { - if (executingError?.message?.startsWith("User rejected the request")) { - addAlert("The transaction signature was declined", { - description: "Nothing will be sent to the network", - timeout: 4 * 1000, - }); - } else { - console.error(executingError); - addAlert("Could not execute the proposal", { - type: "error", - description: "The proposal may contain actions with invalid operations", - }); - } - setIsExecuting(false); - return; - } - - // success - if (!executeTxHash) return; - else if (isConfirming) { - addAlert("Transaction submitted", { - description: "Waiting for the transaction to be validated", - type: "info", - txHash: executeTxHash, - }); - return; - } else if (!isConfirmed) return; - - addAlert("Proposal executed", { - description: "The transaction has been validated", - type: "success", - txHash: executeTxHash, - }); - - setTimeout(() => reload(), 1000 * 2); - }, [executingStatus, executeTxHash, isConfirming, isConfirmed]); - return { executeProposal, canExecute: !isCanVoteError && !isCanVoteLoading && !isConfirmed && !!canExecute, diff --git a/plugins/optimistic-proposals/hooks/useProposalVeto.ts b/plugins/optimistic-proposals/hooks/useProposalVeto.ts index e34fa2f3..59b70443 100644 --- a/plugins/optimistic-proposals/hooks/useProposalVeto.ts +++ b/plugins/optimistic-proposals/hooks/useProposalVeto.ts @@ -1,59 +1,33 @@ -import { useEffect } from "react"; -import { useWaitForTransactionReceipt, useWriteContract } from "wagmi"; import { useProposal } from "./useProposal"; import { useProposalVetoes } from "@/plugins/optimistic-proposals/hooks/useProposalVetoes"; import { useUserCanVeto } from "@/plugins/optimistic-proposals/hooks/useUserCanVeto"; import { OptimisticTokenVotingPluginAbi } from "@/plugins/optimistic-proposals/artifacts/OptimisticTokenVotingPlugin.sol"; -import { useAlerts, type AlertContextProps } from "@/context/Alerts"; import { PUB_DUAL_GOVERNANCE_PLUGIN_ADDRESS } from "@/constants"; import { useProposalId } from "./useProposalId"; +import { useTransactionManager } from "@/hooks/useTransactionManager"; export function useProposalVeto(index: number) { const { proposalId } = useProposalId(index); - const { proposal, status: proposalFetchStatus, refetch: refetchProposal } = useProposal(proposalId, true); const vetoes = useProposalVetoes(proposalId); - - const { addAlert } = useAlerts() as AlertContextProps; - const { writeContract: vetoWrite, data: vetoTxHash, error: vetoingError, status: vetoingStatus } = useWriteContract(); - const { isLoading: isConfirming, isSuccess: isConfirmed } = useWaitForTransactionReceipt({ hash: vetoTxHash }); const { canVeto, refetch: refetchCanVeto } = useUserCanVeto(proposalId); - useEffect(() => { - if (vetoingStatus === "idle" || vetoingStatus === "pending") return; - else if (vetoingStatus === "error") { - if (vetoingError?.message?.startsWith("User rejected the request")) { - addAlert("The transaction signature was declined", { - description: "Nothing will be sent to the network", - timeout: 4 * 1000, - }); - } else { - addAlert("Could not create the proposal", { type: "error" }); - } - return; - } - - // success - if (!vetoTxHash) return; - else if (isConfirming) { - addAlert("Veto submitted", { - description: "Waiting for the transaction to be validated", - txHash: vetoTxHash, - }); - return; - } else if (!isConfirmed) return; - - addAlert("Veto registered", { - description: "The transaction has been validated", - type: "success", - txHash: vetoTxHash, - }); - refetchCanVeto(); - refetchProposal(); - }, [vetoingStatus, vetoTxHash, isConfirming, isConfirmed]); + const { + writeContract, + status: vetoingStatus, + isConfirming, + isConfirmed, + } = useTransactionManager({ + onSuccessMessage: "Veto registered", + onSuccess() { + refetchCanVeto(); + refetchProposal(); + }, + onErrorMessage: "Could not submit the veto", + }); const vetoProposal = () => { - vetoWrite({ + writeContract({ abi: OptimisticTokenVotingPluginAbi, address: PUB_DUAL_GOVERNANCE_PLUGIN_ADDRESS, functionName: "veto", diff --git a/plugins/tokenVoting/hooks/useCreateProposal.ts b/plugins/tokenVoting/hooks/useCreateProposal.ts index 3576033b..5b68a1d6 100644 --- a/plugins/tokenVoting/hooks/useCreateProposal.ts +++ b/plugins/tokenVoting/hooks/useCreateProposal.ts @@ -1,7 +1,6 @@ import { useRouter } from "next/router"; -import { useEffect, useState } from "react"; +import { useState } from "react"; import { ProposalMetadata, RawAction } from "@/utils/types"; -import { useWaitForTransactionReceipt, useWriteContract } from "wagmi"; import { useAlerts } from "@/context/Alerts"; import { PUB_APP_NAME, PUB_CHAIN, PUB_TOKEN_VOTING_PLUGIN_ADDRESS, PUB_PROJECT_URL } from "@/constants"; import { uploadToPinata } from "@/utils/ipfs"; @@ -9,6 +8,7 @@ import { TokenVotingAbi } from "../artifacts/TokenVoting.sol"; import { URL_PATTERN } from "@/utils/input-values"; import { toHex } from "viem"; import { VotingMode } from "../utils/types"; +import { useTransactionManager } from "@/hooks/useTransactionManager"; const UrlRegex = new RegExp(URL_PATTERN); @@ -23,45 +23,18 @@ export function useCreateProposal() { const [resources, setResources] = useState<{ name: string; url: string }[]>([ { name: PUB_APP_NAME, url: PUB_PROJECT_URL }, ]); - const { writeContract: createProposalWrite, data: createTxHash, error, status } = useWriteContract(); - const { isLoading: isConfirming, isSuccess: isConfirmed } = useWaitForTransactionReceipt({ hash: createTxHash }); - useEffect(() => { - if (status === "idle" || status === "pending") return; - else if (status === "error") { - if (error?.message?.startsWith("User rejected the request")) { - addAlert("The transaction signature was declined", { - description: "Nothing will be sent to the network", - timeout: 4 * 1000, - }); - } else { - console.error(error); - addAlert("Could not create the proposal", { type: "error" }); - } - setIsCreating(false); - return; - } - - // success - if (!createTxHash) return; - else if (isConfirming) { - addAlert("Proposal submitted", { - description: "Waiting for the transaction to be validated", - txHash: createTxHash, - }); - return; - } else if (!isConfirmed) return; - - addAlert("Proposal created", { - description: "The transaction has been validated", - type: "success", - txHash: createTxHash, - }); - setTimeout(() => { - push("#/"); - window.scroll(0, 0); - }, 1000 * 2); - }, [status, createTxHash, isConfirming, isConfirmed]); + const { writeContract: createProposalWrite, isConfirming } = useTransactionManager({ + onSuccessMessage: "Proposal created", + onSuccess() { + setTimeout(() => { + push("#/"); + window.scroll(0, 0); + }, 1000 * 2); + }, + onErrorMessage: "Could not create the proposal", + onError: () => setIsCreating(false), + }); const submitProposal = async () => { // Check metadata diff --git a/plugins/tokenVoting/hooks/useProposalExecute.ts b/plugins/tokenVoting/hooks/useProposalExecute.ts index 29b387a8..6cc0113a 100644 --- a/plugins/tokenVoting/hooks/useProposalExecute.ts +++ b/plugins/tokenVoting/hooks/useProposalExecute.ts @@ -1,13 +1,13 @@ -import { useEffect } from "react"; -import { useWaitForTransactionReceipt, useWriteContract, useReadContract } from "wagmi"; +import { useState } from "react"; +import { useReadContract } from "wagmi"; import { TokenVotingAbi } from "../artifacts/TokenVoting.sol"; -import { AlertContextProps, useAlerts } from "@/context/Alerts"; import { useRouter } from "next/router"; import { PUB_CHAIN, PUB_TOKEN_VOTING_PLUGIN_ADDRESS } from "@/constants"; +import { useTransactionManager } from "@/hooks/useTransactionManager"; export function useProposalExecute(proposalId: number) { const { reload } = useRouter(); - const { addAlert } = useAlerts() as AlertContextProps; + const [isExecuting, setIsExecuting] = useState(false); const { data: canExecute, @@ -20,18 +20,26 @@ export function useProposalExecute(proposalId: number) { functionName: "canExecute", args: [BigInt(proposalId)], }); - const { - writeContract: executeWrite, - data: executeTxHash, - error: executingError, - status: executingStatus, - } = useWriteContract(); - const { isLoading: isConfirming, isSuccess: isConfirmed } = useWaitForTransactionReceipt({ hash: executeTxHash }); + + const { writeContract, isConfirming, isConfirmed } = useTransactionManager({ + onSuccessMessage: "Proposal executed", + onSuccess() { + setTimeout(() => reload(), 1000 * 2); + }, + onErrorMessage: "Could not execute the proposal", + onErrorDescription: "The proposal may contain actions with invalid operations", + onError() { + setIsExecuting(false); + }, + }); const executeProposal = () => { if (!canExecute) return; + else if (typeof proposalId === "undefined") return; - executeWrite({ + setIsExecuting(true); + + writeContract({ chainId: PUB_CHAIN.id, abi: TokenVotingAbi, address: PUB_TOKEN_VOTING_PLUGIN_ADDRESS, @@ -40,47 +48,10 @@ export function useProposalExecute(proposalId: number) { }); }; - useEffect(() => { - if (executingStatus === "idle" || executingStatus === "pending") return; - else if (executingStatus === "error") { - if (executingError?.message?.startsWith("User rejected the request")) { - addAlert("Transaction rejected by the user", { - timeout: 4 * 1000, - }); - } else { - console.error(executingError); - addAlert("Could not execute the proposal", { - type: "error", - description: "The proposal may contain actions with invalid operations", - }); - } - return; - } - - // success - if (!executeTxHash) return; - else if (isConfirming) { - addAlert("Proposal submitted", { - description: "Waiting for the transaction to be validated", - type: "info", - txHash: executeTxHash, - }); - return; - } else if (!isConfirmed) return; - - addAlert("Proposal executed", { - description: "The transaction has been validated", - type: "success", - txHash: executeTxHash, - }); - - setTimeout(() => reload(), 1000 * 2); - }, [executingStatus, executeTxHash, isConfirming, isConfirmed]); - return { executeProposal, canExecute: !isCanVoteError && !isCanVoteLoading && !isConfirmed && !!canExecute, - isConfirming, + isConfirming: isExecuting || isConfirming, isConfirmed, }; } diff --git a/plugins/tokenVoting/hooks/useProposalVoting.ts b/plugins/tokenVoting/hooks/useProposalVoting.ts index dd9e6210..f0934a54 100644 --- a/plugins/tokenVoting/hooks/useProposalVoting.ts +++ b/plugins/tokenVoting/hooks/useProposalVoting.ts @@ -1,52 +1,24 @@ -import { useEffect } from "react"; -import { useWriteContract, useWaitForTransactionReceipt } from "wagmi"; import { TokenVotingAbi } from "@/plugins/tokenVoting/artifacts/TokenVoting.sol"; -import { AlertContextProps, useAlerts } from "@/context/Alerts"; import { useRouter } from "next/router"; import { PUB_TOKEN_VOTING_PLUGIN_ADDRESS } from "@/constants"; +import { useTransactionManager } from "@/hooks/useTransactionManager"; export function useProposalVoting(proposalIdx: number) { const { reload } = useRouter(); - const { addAlert } = useAlerts() as AlertContextProps; - const { writeContract: voteWrite, data: votingTxHash, error: votingError, status: votingStatus } = useWriteContract(); - const { isLoading: isConfirming, isSuccess: isConfirmed } = useWaitForTransactionReceipt({ hash: votingTxHash }); - // Loading status and errors - useEffect(() => { - if (votingStatus === "idle" || votingStatus === "pending") return; - else if (votingStatus === "error") { - if (votingError?.message?.startsWith("User rejected the request")) { - addAlert("Transaction rejected by the user", { - timeout: 4 * 1000, - }); - } else { - console.error(votingError); - addAlert("Could not create the proposal", { type: "error" }); - } - return; - } - - // success - if (!votingTxHash) return; - else if (isConfirming) { - addAlert("Vote submitted", { - description: "Waiting for the transaction to be validated", - txHash: votingTxHash, - }); - return; - } else if (!isConfirmed) return; - - addAlert("Vote registered", { - description: "The transaction has been validated", - type: "success", - txHash: votingTxHash, - }); - - reload(); - }, [votingStatus, votingTxHash, isConfirming, isConfirmed]); + const { + writeContract, + status: votingStatus, + isConfirming, + isConfirmed, + } = useTransactionManager({ + onSuccessMessage: "Vote registered", + onSuccess: reload, + onErrorMessage: "Could not submit the vote", + }); const voteProposal = (votingOption: number, autoExecute: boolean = false) => { - voteWrite({ + writeContract({ abi: TokenVotingAbi, address: PUB_TOKEN_VOTING_PLUGIN_ADDRESS, functionName: "vote",