Building a Project with SE-2 | Crowd Fund | Part Five | Multisig Components

WebSculpt
6 min readJan 10, 2024

--

Image from Shubham Dhage on Unsplash

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 👇

Multisig Radio Button

The ☝️Multisig Radio Button☝️ will display options to add one or two more owners to this Fund Run 👇

Radio buttons offer options to add one or two more addresses (as owners) to this Fund Run

Add your other owners to the Fund Run before clicking “START MY FUND”

Here we have added two more owners to this Fund Run before saving it.

The CreateFund Component

CreateFund.tsx

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.

View this new Vault, here.

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

CreateProposal.tsx

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

SupportProposal.tsx

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

FinalizeProposal.tsx

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?

View of the Proposals Table

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:

Simplified view of the Component Hierarchy

--

--

WebSculpt

Blockchain Development, coding on Ethereum. Condensed notes for learning to code in Solidity faster.