从0开发一个Dapp

实现效果

  1. 房主发送拍卖房屋信息,并且设置拍卖过期时间
  1. 其它非房主可以登录进行拍卖,选择自己的出价

参考

https://dev.to/yongchanghe/build-a-simple-dapp-using-truffle-ganache-ethersjs-and-react1-52bl

工具集合

  1. 安装node,版本是v16.16.0
  2. npm install -g truffle, truffle可以让我们在本地开发合约,同时部署合约到链上。
  3. 安装ganache, ganache相当于是把以太网部署在本地,我们可以把合约发布到本地的以太网上。
  4. metamask,钱包可以添加账户,查看我们的账户余额,

初始化项目

  1. mkdir web3-ticket && truffle init
  1. 项目目录如下
  1. 修改truffle.config.js的development
  1. 打开Ganache,新建workspace,选择项目的trffle.config.js

然后我们就可以看到很多账号了,而且里边还有余额

这样我们就可以通过truffle,连接到链上,同时部署合约到这里了

  1. 在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;
 }
}
  1. 执行compile
truffle compile
  1. 添加部署文件,在migrations下面添加2_initial_migrations.js
const Auction = artifacts.require("Auction");

module.exports = function (deployer) {
  deployer.deploy(Auction);
};
  1. 在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

如果出现下图,说明测试成功

完成后目录文件如下

  1. 使用creact-react-app,在web3-ticket目录下

安装依赖

cd client
npm install ethers @ethersproject/units
  1. 修改truuffle.config.js的配置,配置合约的输出目录
  1. 发布合约到本地的链上,也就是Ganache模式的链上,因为前面已经启动过了,这里直接发布就可以。

出现下图,说明已经成功部署。

可以看到,部署成功,因为每次需要消耗ETH,所以这里从100ETH到99.98 ETH

同时可以发现在前端的目录client下,增加了contracts目录,如下图

  1. 修改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;
  1. chrome安装metamask

安装过程比较简单,最主要的是要把钱包的助记词记好😄

这里主要是需要把钱包连接到Ganache启动的链上,点击增加Add Network

把这里的地址复制进去

效果如下图

然后选择连接上Ganache

在Ganache中查看账号,通过私钥导入账号,这里我们导入两个账号


效果如下

  1. 启动前端
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分钟再看

然后我们去我们账户中查看,可以看到

本文由mdnice多平台发布

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,128评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,316评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,737评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,283评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,384评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,458评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,467评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,251评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,688评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,980评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,155评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,818评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,492评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,142评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,382评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,020评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,044评论 2 352

推荐阅读更多精彩内容