Skip to content

Commit

Permalink
Merge pull request #118 from aragon/f/encapsulate-tx-hooks
Browse files Browse the repository at this point in the history
Encapsulate transaction hooks
  • Loading branch information
brickpop authored Sep 2, 2024
2 parents 0b8e2a6 + 99a7885 commit 9993793
Show file tree
Hide file tree
Showing 22 changed files with 391 additions and 885 deletions.
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

0 comments on commit 9993793

Please sign in to comment.