从0开发一个Dapp
实现效果
- 房主发送拍卖房屋信息,并且设置拍卖过期时间
2. 其它非房主可以登录进行拍卖,选择自己的出价
参考
工具集合
- 安装node,版本是v16.16.0
- npm install -g truffle, truffle可以让我们在本地开发合约,同时部署合约到链上。
- 安装ganache, ganache相当于是把以太网部署在本地,我们可以把合约发布到本地的以太网上。
- metamask,钱包可以添加账户,查看我们的账户余额,
初始化项目
- mkdir web3-ticket && truffle init
- 项目目录如下
- 修改truffle.config.js的development
4. 打开Ganache,新建workspace,选择项目的trffle.config.js
然后我们就可以看到很多账号了,而且里边还有余额
这样我们就可以通过truffle,连接到链上,同时部署合约到这里了
- 在web3-ticket/contracts下新建拍卖合约,Auction.sol,代码比较简单,关键地方都已经添加注释。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
contract Auction {
// Properties
// 定义owner地址
address private owner;
uint256 public startTime;
uint256 public endTime;
// 映射
mapping(address => uint256) public bids;
// 结构体
struct House {
string houseType;
string houseColor;
string houseLocation;
}
// 最高出价,竞拍,最高价
struct HighestBid {
uint256 bidAmount;
address bidder;
}
House public newHouse;
HighestBid public highestBid;
// Insert modifiers here
// Modifiers
// 竞拍已经结束
modifier isOngoing() {
require(block.timestamp < endTime, 'This auction is closed.');
_;
}
// 竞拍还在进行
modifier notOngoing() {
require(block.timestamp >= endTime, 'This auction is still open.');
_;
}
// 是不是作者,如果不是没有权限
modifier isOwner() {
require(msg.sender == owner, 'Only owner can perform task.');
_;
}
// 不是作者
modifier notOwner() {
require(msg.sender != owner, 'Owner is not allowed to bid.');
_;
}
// Insert events here
// Events,允许前端调用事件
event LogBid(address indexed _highestBidder, uint256 _highestBid);
event LogWithdrawal(address indexed _withdrawer, uint256 amount);
// Insert constructor and function here
// Assign values to some properties during deployment
constructor () {
owner = msg.sender;
startTime = block.timestamp;
endTime = block.timestamp + 12 hours;
newHouse.houseColor = '#FFFFFF';
newHouse.houseLocation = 'Sask, SK';
newHouse.houseType = 'Townhouse';
}
// makeBid 开始竞价,房子必须是在拍卖中,并且不能房主自己出价
function makeBid() public payable isOngoing() notOwner() returns (bool) {
uint256 bidAmount = bids[msg.sender] + msg.value;
// 当前出价要高于前面的出价,不然报错
require(bidAmount > highestBid.bidAmount, 'Bid error: Make a higher Bid.');
highestBid.bidder = msg.sender;
highestBid.bidAmount = bidAmount;
bids[msg.sender] = bidAmount;
emit LogBid(msg.sender, bidAmount);
return true;
}
// 付款
function withdraw() public notOngoing() isOwner() returns (bool) {
uint256 amount = highestBid.bidAmount;
bids[highestBid.bidder] = 0;
highestBid.bidder = address(0);
highestBid.bidAmount = 0;
// 向房主付款
(bool success, ) = payable(owner).call{ value: amount }("");
require(success, 'Withdrawal failed.');
emit LogWithdrawal(msg.sender, amount);
return true;
}
// 获取最高出价
function fetchHighestBid() public view returns (HighestBid memory) {
HighestBid memory _highestBid = highestBid;
return _highestBid;
}
// 获得当前房主
function getOwner() public view returns (address) {
return owner;
}
}
- 执行compile
truffle compile
7. 添加部署文件,在migrations下面添加2_initial_migrations.js
const Auction = artifacts.require("Auction");
module.exports = function (deployer) {
deployer.deploy(Auction);
};
- 在test下新建测试文件Auctioin.test.js
const Auction = artifacts.require("Auction");
contract("Auction", async (accounts) => {
let auction;
const ownerAccount = accounts[0];
const userAccountOne = accounts[1];
const userAccountTwo = accounts[2];
const amount = 5000000000000000000; // 5 ETH
const smallAmount = 3000000000000000000; // 3 ETH
beforeEach(async () => {
auction = await Auction.new({ from: ownerAccount });
});
it("should make bid.", async () => {
await auction.makeBid({ value: amount, from: userAccountOne });
const bidAmount = await auction.bids(userAccountOne);
assert.equal(bidAmount, amount);
});
it("should reject owner's bid.", async () => {
try {
await auction.makeBid({ value: amount, from: ownerAccount });
} catch (e) {
assert.include(e.message, "Owner is not allowed to bid.");
}
});
it("should require higher bid amount.", async () => {
try {
await auction.makeBid({ value: amount, from: userAccountOne });
await auction.makeBid({ value: smallAmount, from: userAccountTwo });
} catch (e) {
assert.include(e.message, "Bid error: Make a higher Bid.");
}
});
it("should fetch highest bid.", async () => {
await auction.makeBid({ value: amount, from: userAccountOne });
const highestBid = await auction.fetchHighestBid();
assert.equal(highestBid.bidAmount, amount);
assert.equal(highestBid.bidder, userAccountOne);
});
it("should fetch owner.", async () => {
const owner = await auction.getOwner();
assert.equal(owner, ownerAccount);
});
});
然后执行
truffle develop
test
如果出现下图,说明测试成功
完成后目录文件如下
- 使用creact-react-app,在web3-ticket目录下
安装依赖
cd client
npm install ethers @ethersproject/units
- 修改truuffle.config.js的配置,配置合约的输出目录
- 发布合约到本地的链上,也就是Ganache模式的链上,因为前面已经启动过了,这里直接发布就可以。
出现下图,说明已经成功部署。
可以看到,部署成功,因为每次需要消耗ETH,所以这里从100ETH到99.98 ETH
同时可以发现在前端的目录client下,增加了contracts目录,如下图
- 修改client/src/app.js
import "./App.css";
import { useEffect, useState } from "react";
import { ethers } from "ethers";
import { parseEther, formatEther } from "@ethersproject/units";
import Auction from "./contracts/Auction.json";
// 这里的地址是前面合约部署成功之后的合约地址
const AuctionContractAddress = "0xD50acf2Aaa9183b2aC00cd4D27bC3145c919013d";
const emptyAddress = "0x0000000000000000000000000000000000000000";
function App() {
// Use hooks to manage component state
const [account, setAccount] = useState("");
const [amount, setAmount] = useState(0);
const [myBid, setMyBid] = useState(0);
const [isOwner, setIsOwner] = useState(false);
const [highestBid, setHighestBid] = useState(0);
const [highestBidder, setHighestBidder] = useState("");
// Sets up a new Ethereum provider and returns an interface for interacting with the smart contract
// 使用ethers获取合约
async function initializeProvider() {
const provider = new ethers.providers.Web3Provider(window.ethereum);
const signer = provider.getSigner();
return new ethers.Contract(AuctionContractAddress, Auction.abi, signer);
}
// Displays a prompt for the user to select which accounts to connect
// 弹出钱包,让我们选择账户
async function requestAccount() {
const account = await window.ethereum.request({
method: "eth_requestAccounts",
});
setAccount(account[0]);
}
// 获取最高出价
async function fetchHighestBid() {
if (typeof window.ethereum !== "undefined") {
const contract = await initializeProvider();
try {
const highestBid = await contract.fetchHighestBid();
const { bidAmount, bidder } = highestBid;
// Convert bidAmount from Wei to Ether and round value to 4 decimal places
setHighestBid(
parseFloat(formatEther(bidAmount.toString())).toPrecision(4)
);
setHighestBidder(bidder.toLowerCase());
} catch (e) {
console.log("error fetching highest bid: ", e);
}
}
}
async function fetchMyBid() {
if (typeof window.ethereum !== "undefined") {
const contract = await initializeProvider();
try {
const myBid = await contract.bids(account);
setMyBid(parseFloat(formatEther(myBid.toString())).toPrecision(4));
} catch (e) {
console.log("error fetching my bid: ", e);
}
}
}
async function fetchOwner() {
if (typeof window.ethereum !== "undefined") {
const contract = await initializeProvider();
try {
const owner = await contract.getOwner();
setIsOwner(owner.toLowerCase() === account);
} catch (e) {
console.log("error fetching owner: ", e);
}
}
}
// 提价出价
async function submitBid(event) {
event.preventDefault();
if (typeof window.ethereum !== "undefined") {
const contract = await initializeProvider();
try {
// User inputs amount in terms of Ether, convert to Wei before sending to the contract.
const wei = parseEther(amount);
await contract.makeBid({ value: wei });
// Wait for the smart contract to emit the LogBid event then update component state
contract.on("LogBid", (_, __) => {
fetchMyBid();
fetchHighestBid();
});
} catch (e) {
console.log("error making bid: ", e);
}
}
}
// 房屋主人获取转账
async function withdraw() {
if (typeof window.ethereum !== "undefined") {
const contract = await initializeProvider();
// Wait for the smart contract to emit the LogWithdrawal event and update component state
contract.on("LogWithdrawal", (_) => {
fetchMyBid();
fetchHighestBid();
});
try {
await contract.withdraw();
} catch (e) {
console.log("error withdrawing fund: ", e);
}
}
}
useEffect(() => {
requestAccount();
}, []);
useEffect(() => {
if (account) {
fetchOwner();
fetchMyBid();
fetchHighestBid();
}
}, [account]);
return (
<div
style={{
textAlign: "center",
width: "50%",
margin: "0 auto",
marginTop: "100px",
}}
>
{isOwner ? (
<button type="button" onClick={withdraw}>
Withdraw
</button>
) : (
""
)}
<div
style={{
textAlign: "center",
marginTop: "20px",
paddingBottom: "10px",
border: "1px solid black",
}}
>
<p>连接的账户是: {account}</p>
<p>我的出价: {myBid}</p>
<p>拍卖出价最高是: {highestBid}</p>
<p>
拍卖出价最高账户是:{" "}
{highestBidder === emptyAddress
? "null"
: highestBidder === account
? "Me"
: highestBidder}
</p>
{!isOwner ? (
<form onSubmit={submitBid}>
<input
value={amount}
onChange={(event) => setAmount(event.target.value)}
name="Bid Amount"
type="number"
placeholder="Enter Bid Amount"
/>
<button type="submit">提交</button>
</form>
) : (
""
)}
</div>
</div>
);
}
export default App;
- chrome安装metamask
安装过程比较简单,最主要的是要把钱包的助记词记好😄
这里主要是需要把钱包连接到Ganache启动的链上,点击增加Add Network
把这里的地址复制进去
效果如下图
然后选择连接上Ganache
在Ganache中查看账号,通过私钥导入账号,这里我们导入两个账号
效果如下
- 启动前端
npm start
启动服务,会弹出metamask,首先选择Account3,因为Account2是房主自己,Account1是默认添加的。
这里因为是需要输入拍卖假,我们选择10个ETH,查看效果
可以看到这里就弹出metamask,让我们确认我们是否转入10ETH,
我们点击confirm,可以看到我们账号已经少了10ETH
我们再在metamask里边切换会房屋主人的查看页面,选择Account2,看看效果
我们就可以看到最高出价的账户是哪一个。
当我们点击withDraw的时候,发现如下错误
关键的错误是指这里
我们去合约代码Auction.sol里边查看,发现是函数装饰器抛出的错误,因为我们在endTime里边设置的拍卖时间是12个小时,所以这里还在拍卖中,所以房主不能结束拍卖。
// Assign values to some properties during deployment
constructor () {
owner = msg.sender;
startTime = block.timestamp;
endTime = block.timestamp + 12 hours;
newHouse.houseColor = '#FFFFFF';
newHouse.houseLocation = 'Sask, SK';
newHouse.houseType = 'Townhouse';
}
// 竞拍还在进行
modifier notOngoing() {
require(block.timestamp >= endTime, 'This auction is still open.');
_;
}
这里我们可以等待12小时之后,房主再上来点击witdhDraw或者重新编译部署合约, 这里我们选择重新编译部署合约 这里我们把Auction.sol里边的结束时间,设置为5 minutes
constructor () {
owner = msg.sender;
startTime = block.timestamp;
endTime = block.timestamp + 5 minutes;
newHouse.houseColor = '#FFFFFF';
newHouse.houseLocation = 'Sask, SK';
newHouse.houseType = 'Townhouse';
}
重新测试
重新comile
重新部署
这里我们看到部署的地址发生了改变
我们需要把contract address,复制到前端代码的这里替换 在client/src/app.js
// 这里是前面部署合约的时候的contract address
// 最新部署地址
const AuctionContractAddress = "0xAf0C3Db2cC621254E5394B538abdbC50bB860b04";
// 以前部署的地址
// const AuctionContractAddress = "0xe83CfD83BFcb732f207C1Ba94eD4341170667A07";
然后还是先选择Account3 出价竞拍,然后再切换回Account2,查看
等等超过5分钟再看
然后我们去我们账户中查看,可以看到
转载自:https://juejin.cn/post/7144530991472705573