Building a Project with SE-2 | Crowd Fund | Part Three | The Components

WebSculpt
5 min readDec 21, 2023

--

Image from Shubham Dhage on Unsplash

The last blog went over the Contract. This post will cover the frontend components.

If you are new to Scaffold-ETH-2 (SE-2), check out my Intro into Scaffold-ETH-2 blog.
Want to learn a little bit more before you get started? Here’s a post about writing, reading, and listening to Smart Contracts.

Entry Point

It isn’t necessary to add this component to your own project, but it is good for learning-purposes. It simply shows you the most simplistic aspects of The RainbowKit Connection. You can also look into _app.tsx to see how RainbowKitProvider comes into play. We’re a little too under-the-hood at the moment, but if you want to truly understand how SE-2 is offering so much blockchain functionality, those are some good areas to start (as well as the WagmiConfig in _app.tsx).

Zooming out a bit, we can see that Entry Point serves the purpose of showing us some buttons that we can only use after we have connected our wallet:

Wallet is not yet connected
//entry to dapp
return (
<>
<Link href="/crowdfund/start-fund-run" passHref className="link">
<div className="tooltip tooltip-primary" data-tip="Start your Fund Run today!">
<button className="m-2 btn btn-primary">Start Fund Run</button>
</div>
</Link>

<Link href="/crowdfund/browse-fund-runs" passHref className="link">
<div className="tooltip tooltip-primary" data-tip="Donate to projects">
<button className="m-2 btn btn-primary">Start Donating</button>
</div>
</Link>
</>
);
})()}

This ☝️ returns these buttons 👇

If you can see these buttons, then you have successfully connected your wallet to the dApp

Create a Fund Run

Now, let’s learn how our frontend can call the Smart Contract from Part Two.

Check out this component.

All it takes ( THANKS TO SE-2 🥳 ) to call our function createFundRun on the Smart Contract is this…

const { writeAsync, isLoading } = useScaffoldContractWrite({
contractName: "CrowdFund",
functionName: "createFundRun",
args: [titleInput, descInput, targetInput, deadlineInput],
onBlockConfirmation: txnReceipt => {
console.log("📦 Transaction blockHash", txnReceipt.blockHash);
if (txnReceipt.status === "success") {
console.log(txnReceipt);
}
},
});

☝️☝️☝️Note that we are defining the Contract Name as well as the Function Name we are writing to. In this particular instance, we are also sending the args: Title, Description, Target, and Deadline.
This is a non-payable function (on the contract) that we are calling (createFundRun) with SE-2’s custom hook useScaffoldContractWrite, but the same hook can also be used for payable functions as well. You can see in the code-snippet below that there is little difference between the previous example and how we call a payable function (Line 40):

const { writeAsync, isLoading } = useScaffoldContractWrite({
contractName: "CrowdFund",
functionName: "donateToFundRun",
args: [fundRunSingle?.id],
onBlockConfirmation: txnReceipt => {
console.log("📦 Transaction blockHash", txnReceipt.blockHash);
},
value: donationInput,
});

☝️☝️☝️Here we are still using useScaffoldContractWrite, but now we are sending it the ‘Value’ as well (donationInput), and that is how you call a payable function with SE-2.

Use writeAsync like this: writeAsync();
isLoading is a boolean you can use to show progress to the user (with spinners), or you can use it to disable the button that was clicked...

{isLoading ?
<span className="loading loading-spinner loading-sm"></span>
:
<>
DONATE NOW
</>
}

Subscribing to Events with SE-2

When a user creates a Fund Run, we’ll need to redirect them to their new Fund Run. To do this, we’ll subscribe to the FundRunCreated Event with SE-2’s useScaffoldEventSubscriber.
IF we get a new ‘Fund Run Created’ event, then we will want to see if the new Fund Run has an owner that is … well, the user’s address … like this:

useScaffoldEventSubscriber({
contractName: "CrowdFund",
eventName: "FundRunCreated",
listener: logs => {
logs.map(log => {
const { id, owner, title, target } = log.args;
console.log(
"📡 New Fund Run Event \ncreator:",
owner,
"\nID: ",
id,
"\nTitle: ",
title,
"\n with a target of: ",
target,
);
if (userAccount.address == owner) router.push(`/crowdfund/${id}`);
});
},
});

☝️The last line checks if the user’s wallet address is the owner of this (newly-created) Fund Run; IF it is, then it sends the user to that Fund Run’s page.

The Withdrawals

They both look very similar in this scenario…

Here is a bit of the Donor Withdrawal Component:


const { writeAsync, isLoading } = useScaffoldContractWrite({
contractName: "CrowdFund",
functionName: "fundRunDonorWithdraw",
args: [owner.id],
onBlockConfirmation: txnReceipt => {
console.log("📦 Transaction blockHash", txnReceipt.blockHash);
},
});

return (
<>
<button className="btn btn-primary" onClick={() => writeAsync()} disabled={isLoading}>
{isLoading ? <span className="loading loading-spinner loading-sm"></span> : <>Donor Withdraw</>}
</button>
</>
);

And also the Owner Withdrawal Component:


const { writeAsync, isLoading } = useScaffoldContractWrite({
contractName: "CrowdFund",
functionName: "fundRunOwnerWithdraw",
args: [owner.id],
onBlockConfirmation: txnReceipt => {
console.log("📦 Transaction blockHash", txnReceipt.blockHash);
},
});

return (
<>
<button className="btn btn-primary" onClick={() => writeAsync()} disabled={isLoading}>
{isLoading ? <span className="loading loading-spinner loading-sm"></span> : <>Owner Withdraw</>}
</button>
</>
);

As you can see they both use useScaffoldContractWrite, with the only difference being the function they are calling.

Reading from our contract

The useScaffoldContractRead hook is particularly handy. Simply give it a Function Name and a Contract Name:

const { data: fundRuns, isLoading: isListLoading } = useScaffoldContractRead({
contractName: "CrowdFund",
functionName: "getFundRuns",
});

The resulting data will be in fundRuns, and isLoading offers a boolean that I am using with a progress-spinner here:

 if (isListLoading) {
return (
<div className="flex flex-col gap-2 p-2 m-4 mx-auto border shadow-xl border-base-300 bg-base-200 sm:rounded-lg">
<Spinner width="150px" height="150px" />
</div>
);
} else {
return (
<>
{fundRuns?.map(fund => (
<div
key={fund.id.toString()}
...
.....

☝️when done loading, the fundRuns array gets displayed to the user.

The List Component has the Single Components (a .tsx primarily composed of html/tailwind):

 return (
<>
{fundRuns?.map(fund => (
<div
key={fund.id.toString()}
className="flex flex-col gap-2 p-2 m-4 border shadow-xl border-base-300 bg-base-200 sm:rounded-lg"
>
<FundRun
title={fund.title}
description={fund.description}
target={fund.target}
deadline={fund.deadline.toString()}
amountCollected={fund.amountCollected}
amountWithdrawn={fund.amountWithdrawn}
isActive={fund.isActive}
/>

<div className="justify-end card-actions">
<Link href={`/crowdfund/${fund.id}`} passHref className="link">
<div className="tooltip tooltip-primary" data-tip="donate...">
<button className="btn btn-primary">View Fund Run</button>
</div>
</Link>
</div>
</div>
))}
</>
);

Similarly, retrieving just one Fund Run uses a Single FR Component as well.

Here is how to get a single Fund Run by its ID (from the contract):

const { data: fundRunSingle } = useScaffoldContractRead({
contractName: "CrowdFund",
functionName: "getFundRun",
args: fundRun,
});

Part Four is here, with tons of changes. In the next part, the contract will be altered to allow for a Fund Run to have multiple owners; therefore, simple ‘Owner Withdrawals’ will not be sufficient — these owners will have to create/support/partake in a proposals system where they all have to agree upon each transaction, before it can be sent. Click here to check it out.

--

--

WebSculpt

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