实现一个简易的buyMeCoffee网页
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
原学习blog链接
一些疑问:
1.构造函数是payable,部署时不转eth也没有receive和fallback的问题