How to Build a Crowdfunding Web3 Dapp – Let’s Buy Twitter! – DZone Web Dev


For the last several months, the tech world has been abuzz with the news that Elon Musk is buying Twitter. Whether or not the acquisition will actually happen still remains to be seen, but many Twitter employees and Twitter users are concerned about what this may mean for the company culture and for the app itself.

Jokingly I thought to myself, “What if we rallied together and bought Twitter instead?” I don’t have $44 billion, but maybe we could crowdfund it? Surely I could create a GoFundMe or Kickstarter project.

I’ve also recently been delving into the world of Web3, which is all about decentralization. So my next train of thought became, “What would it take to build a crowdfunding app using Web3 technology?”

This article will explore exactly that. We’ll consider how crowdfunding apps normally work, how they would work in the Web3 world, and how we could build our own crowdfunding Web3 decentralized app (“dapp”). We’ll even include some code samples to help you build your own decentralized crowdfunding platform.

Ready to take on Elon?

How Crowdfunding Apps Work

Crowdfunding apps like GoFundMe or Kickstarter allow users to create new fundraisers that anyone can contribute to. The fundraiser creator accepts the donations, usually under certain conditions, and then the crowdfunding platform takes a small percentage of the money as their share. Everybody wins.

For a platform like Kickstarter, the fundraising goal must be met by a deadline to release funds. If the goal is met in time, then the fundraiser creator receives the funds for their project, and all the contributors’ credit cards are charged for the amount they donated. If the deadline passes and the goal is not met, then everyone who contributed gets their money back (or rather, their credit cards are not charged).

This model works pretty well, and plenty of successful projects have been funded by platforms like Kickstarter. But what if we could cut out the middleman?

How a Web3 Crowdfunding Dapp Could Work

Web3 comes with its own transaction layer that allows users to transfer funds held in their crypto wallets. Popular wallets include Coinbase Wallet or MetaMask.

Web3 apps are commonly called “dapps,” due to the decentralized nature of the blockchain. Dapps are built with a frontend UI that interacts with a smart contract deployed to the blockchain, and this smart contract serves as the backend code and database that you’d see in a typical Web2 app.

For a web3 crowdfunding dapp, we could utilize a smart contract that allows people to pledge funds from their wallet toward a cause, just like a Kickstarter campaign. The smart contract could have logic built into it that only allows the crowdfunding project creator to withdraw the funds once the goal has been met. Until then, funds would be held in escrow on the smart contract. This means that donors would have the funds transferred from their wallets when they make their donations, but they could ask for a refund at any time as long as the goal has not yet been met.

Once the goal has been met and the funds have been withdrawn, the person who accepted the donations could do as they pleased with the money, so technically, they could take the money and run. If we wanted to take this idea one step further, we could explore decentralized autonomous organizations (DAOs) and how they handle not just crowdfunding but collective ownership and collective decision making. For now, however, we’ll stick with a simple smart contract only.

So, with that high-level architecture in mind, let’s check out an actual Web3 crowdfunding dapp we built! You can find all of the code for the demo app hosted on GitHub.

Our Web3 Crowdfunding Dapp

Crowdfunding web3 dapp — Let’s buy Twitter!
Our dapp is fairly straightforward from a user standpoint. The user visits the page and clicks the button to connect their wallet. Again, this could be any crypto wallet the user chooses.

If a user does not have a crypto wallet browser extension, clicking the button will prompt Coinbase Wallet’s onboarding UI to pop up, enabling a new user to either connect an existing mobile wallet or create a new wallet in minutes.

Coinbase Wallet’s onboarding UI
Coinbase Wallet’s onboarding UI

Once their wallet is connected, the user can submit a donation by modifying the value in the input field and then clicking the “Donate” button. (We’ve set a minimum donation amount of 0.01 ether and a fund goal of 10 ether in the smart contract, but those values are arbitrary.) They can also click two other buttons to see the total amount contributed toward the goal or to request a refund of the money they previously pledged. There is a button at the bottom of the UI to reset the wallet connection to start over, if needed.

Crowdfunding web3 dapp — Make a donation
Crowdfunding web3 dapp — Make a donation

That’s really all there is to it, functionality-wise.

So, how did we build this? We used several technologies to create our dapp:

  • React for the frontend UI
  • Solidity for the smart contract
  • Remix for compiling and deploying the smart contract
  • Coinbase Wallet SDK for connecting to the user’s wallet
  • Coinbase Wallet and MetaMask crypto wallets for sending and receiving funds
  • Infura for a backup RPC endpoint

We’ve outlined all of the setup steps in the README, so we won’t go into step-by-step detail about how we built the app. If you’d like to follow along or build your own crowdfunding dapp, we’d highly recommend following the steps in the README file above!

Here we highlight two key files that supply the main functionality of the app: the Crowdfunding.sol file for the smart contract, and the App.js file for the React frontend UI.

The Crowdfunding.sol file is reproduced below in full:

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

/*********************************/
/*    Learning Purposes ONLY     */
/*   DO NOT USE IN PRODUCTION    */
/*********************************/

contract Crowdfunding {
    uint256 fundGoal = 10 ether;
    uint256 minContribution = 0.01 ether;

    address payable destinationWallet = payable(0x733B9052fB62C40B344584B20280F6FCcA3D628e);

    mapping(address => uint256) addressContributions;

    function donate() public payable {
        require(msg.value >= minContribution, "Donate Error: Did not meet minimum contribution");
        addressContributions[msg.sender] = msg.value;
    }

    function getBalance() public view returns (uint256) {
        return address(this).balance;
    }

    function withdraw() public {
        require(address(this).balance >= fundGoal, "Withdraw Error: Did not meet contribution goal");
        destinationWallet.transfer(address(this).balance);
    }

    function returnFunds() public {
        require(address(this).balance < fundGoal, "ReturnFunds Error: Cannot refund, goal has been met");
        require(addressContributions[msg.sender] != 0, "ReturnFunds Error: You have not contributed");
        uint256 amount = addressContributions[msg.sender];
        payable(msg.sender).transfer(amount);
    }

    // Need to have a fallback function for the contract to be able to receive funds
    receive() external payable {}
}

This file is what we compiled and deployed from within the Remix online IDE, so it’s not actually included in the project repo. Instead, we reference the address of where the contract was deployed and use the methods defined in the contract’s application binary interface (ABI).

Scanning through this file, you can see that we’ve defined methods for donate, getBalance, withdraw, and returnFunds. Each method does what its name implies.

  • The donate method allows users to pledge donations.
  • The getBalance method shows the current total amount of donations contributed.
  • The withdraw method allows the funds to be withdrawn under the condition that the fundraiser goal has been met.
  • The returnFunds method allows users to request a refund of their pledged amount if they change their minds after contributing.

Now let’s look at the frontend code with our App.js file, which is also reproduced in full below:

import React, { useEffect, useState } from 'react';
import Web3 from 'web3';
import Contract from 'web3-eth-contract';
import CoinbaseWalletSDK from '@coinbase/wallet-sdk';
import CrowdfundingContract from './contracts/Crowdfunding.json';
import elon from './elon.jpg';
import './App.css';

const APP_NAME = 'Coinbase Crowdfunding App';
const APP_LOGO_URL = './elon.jpg';
const RPC_URL = process.env.REACT_APP_INFURA_RPC_URL;
const CHAIN_ID = 3; // Ropsten Network ID
const CROWDFUNDING_CONTRACT_ADDRESS =
  '0x6CE498a35a39Cb43c08B81e7A06f2bb09741359d';

const App = () => {
  const [isWalletConnected, setIsWalletConnected] = useState(false);
  const [account, setAccount] = useState();
  const [walletSDKProvider, setWalletSDKProvider] = useState();
  const [web3, setWeb3] = useState();
  const [crowdfundingContractInstance, setCrowdfundingContractInstance] =
    useState();
  const [responseMessage, setResponseMessage] = useState();

  useEffect(() => {
    // Initialize Coinbase Wallet SDK
    const coinbaseWallet = new CoinbaseWalletSDK({
      appName: APP_NAME,
      appLogoUrl: APP_LOGO_URL,
    });

    // Initialize Web3 Provider
    const walletSDKProvider = coinbaseWallet.makeWeb3Provider(
      RPC_URL,
      CHAIN_ID
    );
    setWalletSDKProvider(walletSDKProvider);

    // Initialize Web3 object
    const web3 = new Web3(walletSDKProvider);
    setWeb3(web3);

    // Initialize crowdfunding contract
    const web3ForContract = new Web3(window.ethereum);
    Contract.setProvider(web3ForContract);
    const crowdfundingContractInstance = new Contract(
      CrowdfundingContract,
      CROWDFUNDING_CONTRACT_ADDRESS
    );
    setCrowdfundingContractInstance(crowdfundingContractInstance);
  }, []);

  const checkIfWalletIsConnected = () => {
    if (!window.ethereum) {
      console.log(
        'No ethereum object found. Please install Coinbase Wallet extension or similar.'
      );

      // Enable the provider and cause the Coinbase Onboarding UI to pop up
      web3.setProvider(walletSDKProvider.enable());

      return;
    }

    console.log('Found the ethereum object:', window.ethereum);
    connectWallet();
  };

  const connectWallet = async () => {
    const accounts = await window.ethereum.request({
      method: 'eth_requestAccounts',
    });

    if (!accounts.length) {
      console.log('No authorized account found');
      return;
    }

    if (accounts.length) {
      const account = accounts[0];
      console.log('Found an authorized account:', account);
      setAccount(account);

      try {
        await window.ethereum.request({
          method: 'wallet_switchEthereumChain',
          params: [{ chainId: '0x3' }],
        });
        console.log('Successfully switched to Ropsten Network');
      } catch (error) {
        console.error(error);
      }
    }

    setIsWalletConnected(true);
  };

  const donateETH = async () => {
    if (!account || !window.ethereum) {
      console.log('Wallet is not connected');
      return;
    }

    const donationAmount = document.querySelector('#donationAmount').value;

    const response = await crowdfundingContractInstance.methods.donate().send({
      from: account,
      value: donationAmount,
    });

    console.log(response);
    setResponseMessage(
      `Thank you for donating! Here's your receipt: ${response.transactionHash}`
    );
  };

  const getDonationBalance = async () => {
    const response = await crowdfundingContractInstance.methods
      .getBalance()
      .call();
    setResponseMessage(
      `Total contribution amount is ${web3.utils.fromWei(response)} ETH.`
    );
  };

  const requestRefund = async () => {
    await crowdfundingContractInstance.methods
      .returnFunds()
      .send({ from: account });
    setResponseMessage('Your donation has been refunded.');
  };

  const resetCoinbaseWalletConnection = () => {
    walletSDKProvider.close();
  };

  return (
    <main className="app">
      <header>
        <img
          src={elon}
          className="headerImage"
          alt="Elon holding the Twitter logo"
        />
        <h1>Let's buy Twitter before Elon does!</h1>
      </header>

      {isWalletConnected ? (
        <>
          <p>Connected Account: {account}</p>
          <div>
            <input
              type="number"
              id="donationAmount"
              defaultValue={10000000000000000}
            />
            <label htmlFor="donationAmount">WEI</label>
            <button onClick={donateETH} id="donate" type="button">
              Donate
            </button>
          </div>
          <div>
            <button
              id="getDonationBalance"
              type="button"
              onClick={getDonationBalance}
            >
              See Total Contribution Amount
            </button>
          </div>
          <div>
            <button id="requestRefund" type="button" onClick={requestRefund}>
              Request Refund
            </button>
          </div>
          <div>
            <button
              id="reset"
              type="button"
              onClick={resetCoinbaseWalletConnection}
            >
              Reset Connection
            </button>
          </div>
        </>
      ) : (
        <button onClick={checkIfWalletIsConnected} id="connect" type="button">
          Connect Wallet
        </button>
      )}
      <p>{responseMessage}</p>
    </main>
  );
};

export default App;

There’s a lot of code in this file, but let’s discuss a few highlights. As you can see, we use the Coinbase Wallet SDK for connecting to the user’s wallet. We load our crowdfunding contract using the contract’s ABI and deployed address. We interact with the smart contract’s methods by using .call() and .send(), and we up-clicked handlers to our buttons to make the app interactive.

At a high level, that is the magic behind how all this works. For more detailed setup instructions, we would again refer you to the step-by-step guide found in the README on GitHub.

Conclusion

So, what have we learned today?

We’ve learned that Web3 technology allows for financial transactions without an intermediary institution. We’ve learned that besides transferring money from one individual to another, we can also use Web3 technology to support crowdfunding.

Finally, we’ve explored how a simple crowdfunding app might be built, the technologies behind it, and how using these technologies together can enable you to have an dapp up and running in a matter of hours.

Quoted from Various Sources

Published for: The Bloggers Briefing