Building a Project with SE-2 | Crowd Fund | Part Five | Multisig Components
If you are here for the first time, then you may want to start at the beginning of this series.
Other posts in the series:
V1’s Smart Contract
V1’s Components
View the Previous Post: V2’s Smart Contract
This post will cover the front-end Components that are interacting with our Smart Contract.
**Note that the readme file has a demo video to better-convey some of these points.
I have provided examples for how to interact with your smart contract using BOTH ethers.js and viem.
The test file uses ethers.js
The components are using viem
The previous post goes over an ethers.js _> viem migration in more detail
Creating a new Fund Run
View the live page for reference.
To create a new, multisig Fund Run — click on the Multisig Radio Button 👇
The ☝️Multisig Radio Button☝️ will display options to add one or two more owners to this Fund Run 👇
Add your other owners to the Fund Run before clicking “START MY FUND”
The CreateFund Component
This is what the final call (that creates this Fund Run) looks like:
const { writeAsync, isLoading } = useScaffoldContractWrite({
contractName: "CrowdFund",
functionName: "createFundRun",
args: [titleInput, descInput, targetInput, deadlineInput, ownersList],
onBlockConfirmation: txnReceipt => {
console.log("📦 Transaction blockHash", txnReceipt.blockHash);
},
});
Once this new Fund Run has completed (and has been successfully funded), then the 3 owners can create Proposals.
Here is a look at a newly created Proposal. Note that only the user who created the Proposal can Revoke it.
☝️This “Proposal” is the result of ‘0x1e…9350’ proposing to pay 0.1 Ether to ‘0xEd…0676’ (for the reason: “solidity dev”). This means that ‘0x1e…9350’ first signed the message, and then they interacted with the Smart Contract in order to store the Signature along with the Proposal details.
To progress this along further, the other owners will have to click on the “support” button, Sign the Message, and then confirm to store the signature on the contract.
Then, any of the owners can send these funds by clicking the “Finalize” button…
You will then see the Proposal row reflect this change…
The CreateProposal Component
After a user Signs this Message 👇
const signNewProposal = async () => {
const nonce = getNonce(fundRunNonce);
const digest = await getDigest(nonce, parseEther(transferInput), toAddressInput, userAddress.address, reasonInput);
const proposalCreationSig: any = await walletClient?.signMessage({
account: walletClient.account,
message: { raw: toBytes(digest) },
});
setCreationSignature(proposalCreationSig);
};
Then we will utilize the Reactive nature of React — when the signature is set, we will call the createMultisigProposal
function on the Smart Contract:
useEffect(() => {
if (creationSignature !== undefined) {
writeAsync();
}
}, [creationSignature]);
const { writeAsync, isLoading } = useScaffoldContractWrite({
contractName: "CrowdFund",
functionName: "createMultisigProposal",
args: [
creationSignature,
fundRun?.fundRunId,
{
amount: parseEther(transferInput),
to: toAddressInput,
proposedBy: userAddress.address,
reason: reasonInput,
},
],
onBlockConfirmation: txnReceipt => {
console.log("📦 Transaction blockHash", txnReceipt.blockHash);
},
onError: err => {
console.log("Transaction Error Message", err?.message);
},
});
Need a refresher for contract-interaction with SE-2? Here you go.
The SupportProposal Component
This particular component is displaying this Support button👇
But the Proposal Data, itself, is coming from the getProposals
function on the Smart Contract. We then use that Proposal Data in a similar way to when we first created this Proposal (to Sign a Message with the correct Digest):
const supportProposal = async () => {
const nonce = getNonce(fundRunNonce);
const digest = await getDigest(nonce, proposal.amount, proposal.to, proposal.proposedBy, proposal.reason);
const proposalSupportSig: any = await walletClient?.signMessage({
account: walletClient.account,
message: { raw: toBytes(digest) },
});
setSupportSignature(proposalSupportSig);
};
When the signature is set, we will call the supportMultisigProposal
function on the Smart Contract:
useEffect(() => {
if (supportSignature !== undefined) {
writeAsync();
}
}, [supportSignature]);
const { writeAsync, isLoading } = useScaffoldContractWrite({
contractName: "CrowdFund",
functionName: "supportMultisigProposal",
args: [supportSignature, proposal?.fundRunId, proposal?.proposalId],
onBlockConfirmation: txnReceipt => {
console.log("📦 Transaction blockHash", txnReceipt.blockHash);
},
onError: err => {
console.log("Transaction Error Message", err?.message);
},
});
Need a refresher for contract-interaction with SE-2? Here you go.
The FinalizeProposal component
When it comes time to “Finalize” a Proposal, you are saying that everyone has signed and you are ready to actually perform this (proposed) transaction. There is no longer any need to sign a message, as all the signatures should already be in-place; thus, Proposal Finalization is as simple as calling multisigWithdraw
, like this:
const tx = { amount: proposal.amount, to: proposal.to, proposedBy: proposal.proposedBy, reason: proposal.reason };
useEffect(() => {
if (nonce !== undefined) {
writeAsync();
}
}, [nonce]);
.....
...
const finishProposal = () => {
const nonce = getNonce(fundRunNonce);
setNonce(nonce);
};
.....
...
const { writeAsync, isLoading } = useScaffoldContractWrite({
contractName: "CrowdFund",
functionName: "multisigWithdraw",
args: [tx, nonce, proposal.fundRunId, proposal.proposalId],
onBlockConfirmation: txnReceipt => {
console.log("📦 Transaction blockHash", txnReceipt.blockHash);
},
onError: err => {
console.log("Transaction Error Message", err?.message);
},
});
How does the Table work?
ListProposals.tsx calls the function: getProposals
(on the Smart Contract), and then maps each of these (returned) Proposals to the SingleProposal.tsx Component.
export const ListProposals = (frVault: ListProposalProps) => {
const { data: vaultProposals, isLoading: isListLoading } = useScaffoldContractRead({
contractName: "CrowdFund",
functionName: "getProposals",
args: [frVault.fundRunId],
});
if (isListLoading) {
return (
<div className="flex......">
<Spinner width="150px" height="150px" />
</div>
);
} else {
return (
<>
<div className="flex ......">
<div className="w-full...">
<table className="table....">
<thead>
<tr className="text-sm rounded-xl text-base-content">
<th className="bg-primary">Status</th>
<th className="bg-primary">ID</th>
...table-headers...
</tr>
</thead>
<tbody>
{vaultProposals?.map(vp =>
vp.to !== "0x0000000000000000000000000000000000000000" &&
vp.proposedBy !== "0x0000000000000000000000000000000000000000" ? (
<tr
className={`text-sm ${vp.status == 0 ? "bg-secondary border-secondary" : ""} ${
vp.status == 1 ? "bg-accent border-accent" : ""
} ${vp.status == 2 ? "bg-neutral border-neutral text-primary" : ""}`}
key={vp.proposalId.toString()}
>
<SingleProposal
proposalId={vp.proposalId}
fundRunId={frVault.fundRunId}
status={vp.status}
amount={vp.amount}
to={vp.to}
proposedBy={vp.proposedBy}
reason={vp.reason}
/>
</tr>
) : null,
)}
</tbody>
</table>
</div>
</div>
</>
);
}
};
And then the SingleProposal Component has the SupportProposal, RevokeProposal, and FinalizeProposal Components👇
export const SingleProposal = (proposal: DisplayProposalProps) => {
return (
<>
<td className="w-1/12 md:py-4">
{proposal.status === 0 && <>😄</>}
{proposal.status === 1 && <>🤝</>}
{proposal.status === 2 && <>✅</>}
</td>
<td className="w-1/12 md:py-4">{proposal.proposalId.toString()}</td>
<td className="w-1/12 md:py-4">{formatEther(proposal.amount)}</td>
<td className="w-1/12 md:py-4">{proposal.to}</td>
<td className="w-1/12 md:py-4">{proposal.proposedBy}</td>
<td className="w-1/12 md:py-4">{proposal.reason}</td>
{proposal.status !== 2 ? (
<>
<SupportProposal
fundRunId={proposal.fundRunId}
proposalId={proposal.proposalId}
amount={proposal.amount}
to={proposal.to}
proposedBy={proposal.proposedBy}
reason={proposal.reason}
/>
{proposal.status !== 0 ? (
<FinalizeProposal
fundRunId={proposal.fundRunId}
proposalId={proposal.proposalId}
amount={proposal.amount}
to={proposal.to}
proposedBy={proposal.proposedBy}
reason={proposal.reason}
/>
) : (
<td className="w-1/12 md:py-4"></td>
)}
<RevokeProposal fundRunId={proposal.fundRunId} proposalId={proposal.proposalId} />
</>
) : (
<>
<td className="w-1/12 md:py-4"></td>
<td className="w-1/12 md:py-4"></td>
<td className="w-1/12 md:py-4"></td>
</>
)}
</>
);
};
A clearer view of the Component Hierarchy:
Ready to learn more? Crowd Fund V3 will take these Proposals off-chain via a subgraph.