Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Encapsulate transaction hooks #118

Merged
merged 2 commits into from
Sep 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions hooks/useTransactionManager.ts
Original file line number Diff line number Diff line change
@@ -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 };
}
54 changes: 13 additions & 41 deletions plugins/emergency-multisig/hooks/useCreateProposal.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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);

Expand All @@ -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
Expand Down
19 changes: 8 additions & 11 deletions plugins/emergency-multisig/hooks/useProposalApprovals.ts
Original file line number Diff line number Diff line change
@@ -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<ApprovedEvent[]>([]);

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),
Expand All @@ -36,7 +33,7 @@ export function useProposalApprovals(

useEffect(() => {
getLogs();
}, [proposal?.parameters?.snapshotBlock]);
}, [!!publicClient, proposal?.parameters?.snapshotBlock]);

return proposalLogs;
}
82 changes: 21 additions & 61 deletions plugins/emergency-multisig/hooks/useProposalApprove.ts
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -81,7 +41,7 @@ export function useProposalApprove(proposalId: string) {
proposalFetchStatus,
approvals,
canApprove: !!canApprove,
isConfirming: approveStatus === "pending" || isConfirming,
isConfirming: status === "pending" || isConfirming,
isConfirmed,
approveProposal,
};
Expand Down
71 changes: 19 additions & 52 deletions plugins/emergency-multisig/hooks/useProposalExecute.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
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";
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();
Expand All @@ -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;
Expand All @@ -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,
Expand All @@ -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:
Expand Down
Loading
Loading