2.“buy me a coffee” DAPP实例

291 阅读3分钟

实现一个简易的buyMeCoffee网页

buyMeCoffee.png

dapp功能:

1.输入留言和姓名,点击赞助,触发交易

2.交易完成,更新渲染

步骤:

1.创建合约项目文件夹,进入文件夹并初始化

npm init -y

npm install --save-dev hardhat

npx hardhat

2.安装依赖

npm install --save-dev @nomiclabs/hardhat-waffle ethereum-waffle chai @nomiclabs/hardhat-ethers ethers

3.主合约 /contracts/cofferSeller.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;

contract CoffeeSeller {

    uint256 public totalCoffees;
    // 接收赞助地址
    address payable public owner;

    // 前端监听事件,更新渲染
    event NewCoffee(
        address indexed from,
        string name,
        string message,
        uint256 timestamp
    );
    
    // 获取合约的创建者地址 
    constructor() payable {
        //可以通过 payable(x) 将 address 转换为 address payable, 其中 x 必须是 address 类型
        owner = payable(msg.sender);
    }
    
    struct Coffee {
        // 赞助人地址
        address giver;
        // 昵称
        string name;
        // 留言
        string message;
        // 时间
        uint256 timestamp;
    }

    // 记录捐赠的数组
    Coffee[] public coffees;


    // 新增赞助,这里直接以调用时发送的eth数目msg.value做赞助值
    function buyCoffee(string memory _name,string memory _message) public payable{
        uint256 price = 0.001 ether;

        require(msg.value >= price, "our coffee starts at 0.001 ether");
        
        coffees.push(Coffee(msg.sender, _name, _message,block.timestamp));
        totalCoffees += 1;

        (bool success,) = owner.call{value:msg.value}("");
        require(success,"failed to buyCoffee");
        
  
        emit NewCoffee(msg.sender, _name, _message,block.timestamp);
    }

    // 获取赞助的总数
    function getTotalCoffees() public view returns(uint256) {
        return totalCoffees;
    }

    // 返回数组以便循环生成所有的信息
    function getCoffees() public view returns (Coffee[] memory) {
        return coffees;
    }

    
}

4.部署脚本/scripts/deploy.js 并运行

const hre = require("hardhat");

async function main() {
  // 获取部署者账户
  const [deployer] = await hre.ethers.getSigners();
  console.log(`contract is deployed by account:${deployer.address}`);

// 获取合约实例
  const CoffeeSeller = await hre.ethers.getContractFactory("CoffeeSeller");
  // 部署合约
  const coffeeSeller = await CoffeeSeller.deploy();

  // 等待合约部署完成
  await coffeeSeller.deployed()

  console.log(
    `contract deployed to ${coffeeSeller.address}`
  );
}

// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

运行合约的配置见作者的另一篇[文章]中hardhat部分(juejin.cn/post/722117…)

获得合约的部署地址

5.前端配置react-tailwind工程

终端输入

npx create-react-app 前端项目名

tailwindcss引入作者的另一篇文章

6.安装ethers依赖

终端输入

npm install ethers 

// 弹出的进度条警告框之类
npm install react-tostify

7.app.js文件

/*
 * @Author: diana
 * @Date: 2023-05-05 21:52:34
 * @LastEditTime: 2023-05-06 19:50:35
 */
import React, { useEffect, useState } from 'react';
import { ethers } from 'ethers';

import { ToastContainer, toast } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import "./index.css";

import abi from './CoffeeSeller.json'

function App() {

  // 合约地址abi
  const contractAddress = '';
  //合约编译的json文件
  const contractABI = abi.abi;

  // 接收参数
  const [currentAccount, setCurrentAccount] = useState("");
  const [name, setName] = useState("");
  const [message, setMessage] = useState("");

  const [allCoffee, setAllCoffee] = useState([]);

  // 检查是否连接钱包
  const checkIfWalletIsConnected = async () => {
    try {
      const { ethereum } = window;
      // 获取当前网页连接的账户
      const accounts = await ethereum.request({ method: 'eth_accounts' })

      if (accounts.length !== 0) {
        const currentAc = accounts[0];
        setCurrentAccount(currentAc);

        toast.success("Wallet is Connected", {
          position: "top-right",
          autoClose: 5000,
          hideProgressBar: false,
          closeOnClick: true,
          pauseOnHover: true,
          draggable: true,
          progress: undefined,
        })
      } else {
        toast.warn("Make sure you have MetaMask Connected", {
          position: "top-right",
          autoClose: 5000,
          hideProgressBar: false,
          closeOnClick: true,
          pauseOnHover: true,
          draggable: true,
          progress: undefined,
        });
      }

    } catch (e) {
      toast.error(`${e.message}`, {
        position: "top-right",
        autoClose: 5000,
        hideProgressBar: false,
        closeOnClick: true,
        pauseOnHover: true,
        draggable: true,
        progress: undefined,
      });
    }
  }

  // 连接钱包
  const connectWallet = async () => {
    try {
      const { ethereum } = window
      if (!ethereum) {
        toast.warn("Make sure you have MetaMask Connected", {
          position: "top-right",
          autoClose: 5000,
          hideProgressBar: false,
          closeOnClick: true,
          pauseOnHover: true,
          draggable: true,
          progress: undefined,
        });
        return;
      }
      // 申请连接,弹窗
      const account = ethereum.request({ method: 'eth_requestAccounts' })
      setCurrentAccount(account[0]);
    } catch (e) {
      toast.error(`${e.message}`, {
        position: "top-right",
        autoClose: 5000,
        hideProgressBar: false,
        closeOnClick: true,
        pauseOnHover: true,
        draggable: true,
        progress: undefined,
      })
    }
  }

  // 购买
  const buyCoffee = async () => {
    try {
      const { ethereum } = window;

      if (ethereum) {
        const provider = new ethers.providers.Web3Provider(ethereum);
        // 写操作必须要signer
        const signer = provider.getSigner();

        // 合约对象
        const coffeeContract = new ethers.Contract(
          contractAddress,
          contractABI,
          signer
        );

        const coffeeTxn = await coffeeContract.buyCoffee(
          name ? name : "Anonyous",
          message ? message : "enjoy your coffee",
          
          //value时调用时要传给合约的eth
          { value: ethers.utils.parseEther("0.001"), gasLimit: 400000 },
        );

        console.log("Mining...", coffeeTxn.hash);

        toast.info("Sending Fund for coffee...", {
          position: "top-left",
          autoClose: 18050,
          hideProgressBar: false,
          closeOnClick: true,
          pauseOnHover: true,
          draggable: true,
          progress: undefined,
        });

        await coffeeTxn.wait();

        console.log("Mined", coffeeTxn.hash);

        setMessage("");
        setName("");

        toast.success("Coffee Purchased!", {
          position: "top-left",
          autoClose: 5000,
          hideProgressBar: false,
          closeOnClick: true,
          pauseOnHover: true,
          draggable: true,
          progress: undefined,
        });
      } else {
        console.log("Ethereum object doesn't exist!");
      }
    } catch (e) {
      console.log(e)
    }
  }

  // 获取已购买的数组以便展示
  const getAllCoffee = async () => {
    try {
      const { ethereum } = window
      if (ethereum) {
        const provider = new ethers.providers.Web3Provider(ethereum);
        const signer = provider.getSigner();
        const coffeeContract = new ethers.Contract(
          contractAddress,
          contractABI,
          signer
        );

        const coffees = await coffeeContract.getCoffees();

        const finalCoffees = coffees.map(item => {
          return {
            address: item.giver,
            name: item.name,
            message: item.message,
            timestamp: new Date(item.timestamp * 1000)
          }
        });

        setAllCoffee(finalCoffees)

        console.log("------------#---------------", finalCoffees);
      } else {
        console.log("Ethereum object doesn't exist!");
      }
    } catch (e) {
      console.log(e);
    }
  }

  useEffect(() => {
    let coffeeContract;
    checkIfWalletIsConnected();

    getAllCoffee();

    // 监听时调用的函数
    const onNewCoffee = (from, name, message, timestamp) => {
      setAllCoffee(pre =>
        [...pre, {
          address: from,
          name: name,
          message: message,
          timestamp: new Date( timestamp * 1000)
        }] 
      );
    };

    if (window.ethereum) {
      const provider = new ethers.providers.Web3Provider(window.ethereum);
      const signer = provider.getSigner();

      coffeeContract = new ethers.Contract(
        contractAddress,
        contractABI,
        signer
      );
      coffeeContract.on("NewCoffee", onNewCoffee);
    }

    return () => {
      // 关闭订阅的监听 
      if (coffeeContract) {
        coffeeContract.off("NewCoffee", onNewCoffee);
      }
    }
  }, [])

  // 输入框值变化事件
  const handleOnMessageChange = (e) => {
    const { value } = e.target;
    setMessage(value);
  }
  const handleOnNameChange = (e) => {
    const { value } = e.target;
    setName(value);
  };

  return (
    <div className="flex flex-col items-center justify-center min-h-screen py-1 bg-gray-100">

      <main className='flex flex-col items-center justify-center w-full flex-1 px-20 text-center'>
        <h1 className='text-6xl text-green-600 mb-6'>BUY ME A COFFEE</h1>

        {
          currentAccount ?
            (
              <div className='w-full max-w-xs top-3 z-50'>
                <form className='bg-white shadow-md-rounded px-8 pt-6 pb-8 mb-4'>
                  <div className="mb-4">
                    <label
                      className="block text-gray-700 text-sm font-bold mb-2"
                      htmlFor="name"
                    >
                      Name
                    </label>
                    <input
                      className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
                      id="name"
                      type="text"
                      placeholder="Name"
                      onChange={handleOnNameChange}
                      required
                    />
                  </div>

                  <div className="mb-4">
                    <label
                      className="block text-gray-700 text-sm font-bold mb-2"
                      htmlFor="message"
                    >
                      Send the Creator a Message
                    </label>

                    <textarea
                      className="form-textarea mt-1 block w-full shadow appearance-none py-2 px-3 border rounded text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
                      rows="3"
                      placeholder="Message"
                      id="message"
                      onChange={handleOnMessageChange}
                      required
                    ></textarea>
                  </div>

                  <div className="flex items-left justify-between">
                    <button
                      className="bg-green-500 hover:bg-green-700 text-center text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"
                      type="button"
                      onClick={buyCoffee}
                    >
                      Support
                    </button>
                  </div>
                </form>

              </div>

            ) : (
              <button className='bg-green-500 hover:bg-green-700 text-white font-vold' onClick={connectWallet}>
                connect your wallet
              </button>
            )
        }

        {allCoffee.map((coffee, index) => {
          return (
            <div className="border-l-2 mt-10" key={index}>
              <div className="transform transition cursor-pointer hover:-translate-y-2 ml-10 relative flex items-center px-6 py-4 bg-green-600 text-white rounded mb-10 flex-col md:flex-row space-y-4 md:space-y-0">
                {/* <!-- Dot Following the Left Vertical Line --> */}
                <div className="w-5 h-5 bg-green-600 absolute -left-10 transform -translate-x-2/4 rounded-full z-10 mt-2 md:mt-0"></div>

                {/* <!-- Line that connecting the box with the vertical line --> */}
                <div className="w-10 h-1 bg-green-300 absolute -left-10 z-0"></div>

                {/* <!-- Content that showing in the box --> */}
                <div className="flex-auto">
                  <h1 className="text-md">Supporter: {coffee.name}</h1>
                  <h1 className="text-md">Message: {coffee.message}</h1>
                  <h3>Address: {coffee.address}</h3>
                  <h1 className="text-md font-bold">
                    TimeStamp: {coffee.timestamp.toString()}
                  </h1>
                </div>
              </div>
            </div>
          );
        })}

        <ToastContainer
          position="top-right"
          autoClose={5000}
          hideProgressBar={false}
          newestOnTop={false}
          closeOnClick
          rtl={false}
          pauseOnFocusLoss
          draggable
          pauseOnHover
        />

      </main>


    </div>
  );
}

export default App;

8.终端运行

npm run start

run.png

原学习blog链接

一些疑问:

1.构造函数是payable,部署时不转eth也没有receive和fallback的问题