以太坊Dapp项目-拍卖网站-智能合约编写测试

修订日期 姓名 邮箱
2018-10-18 brucefeng [email protected]

前言

写这篇文章的初衷其实很简单,在MyEtherWallet上申请以太坊ENS的时候,竞标的以太币两次被吞,而且是在规定时间点进行了价格公告,这篇文章的设计思路其实就是跟ENS的竞标流程类似,希望对大家有所帮助,所以,准备写完之后,再重新去整一次ENS的申请,如果再被吞,我就要举报了:-),本文主要是本人用于项目整理,便于自己查询,不做任何商业用途。

现在回归到技术上来,这个项目其实涉及到蛮多的知识点的,是非常不错的以太坊智能合约以及Dapp学习项目,至少在目前而言,还没有看到特别好的学习项目被分享出来,通过该项目,我们可以掌握如下内容:

  • 以太坊智能合约编程语言Solidity的编写
  • 智能合约框架Truffle的学习与使用
  • 以太坊与IPFS的整合
  • NodeJS编程学习
  • 以太坊Web3JS的接口学习
  • Dapp与主流数据库的整合(本文为NoSQL类型的MongoDB)
  • 维克里拍卖法则

一.项目介绍

1.项目功能

(1)项目展示

允许商家列出项目,我们将为任何人建立免费列出项目的功能,我们会将这些项目都存储在区块链和非区块链的数据库中,方便查询。

(2) 文件存储

将文件添加到IPFS:我们将商品图像和商品描述(大文本)上传至IPFS的功能。

(3)浏览商品

我们将添加根据类别,拍卖时间等过滤和浏览商品的功能。

(4)商品拍卖

实现维克里密封拍卖,招标流程跟ENS类似。

(5)托管合约

一旦投标结束,商品有赢家,我们将在买方,卖方和第三方仲裁人之间创建一个托管合同

(6) 2-of-3数字签名

我们将通过2-of-3数字,其中3名参与者中的2名必须投票将资金释放给卖方或者将金额退还给卖方。

2.项目架构

以下图片来源于网络

以太坊Dapp项目-拍卖网站-智能合约编写测试

(1) Web前端

HTML,CSS,JavaScript(大量使用web3js),用户将通过这个前端应用程序与区块链,IPFS和NodeJS服务器进行交互

(2) 区块链

这是所有代码和交易所在的应用程序的核心,商店中所有商品,用户出价和托管都写在区块链上。

(3) NodeJS服务器

这是前端通过其与数据库进行通信的后端服务器,我们将公开一些简单的API来为前端查询和从数据库中检索商品。

(4) MongoDB

尽管商品存储在区块链中,但是查询区块链展示商品和应用各种过滤器(仅显示特定类别的商品,显示即将过期的商品等)效率并不高,我们将使用MongoDB数据库来存储商品信息并查询它以展示商品。

(5)区块链存储IPFS

当用户在商店中列出商品时,前端会将商品文件和描述上传至IPFS,并将上传文件的散列HASH存储到区块链中。

3. 业务流向

以太坊Dapp项目-拍卖网站-智能合约编写测试

(1) 用户访问前端

(2) 将商品文件与描述信息传至IPFS中

(3) IPFS返回对应的Hash值

(4) 网页前端调用合约将Hash值结合产品ID,拍卖时间,分类,价格等写入区块链中

(5) 从区块链中读取数据展示在web前端

(6) NodeJs服务器监听这些事件,当事件被合约触发时,服务器从区块链中取出数据缓存至mongodb中。

4. 实现步骤

  • 先通过truffle 和 solidity实现合约代码,将其部署到truffle develop自带的测试网络中,并且在truffle console中可以自由交互。

  • 通过命令行安装并与IPFS交互

  • 在后端实现完成后,我们将构建Web前端以与合约和IPFS进行交互,我们也会实现招标,揭示前端的拍卖功能。

  • 我们将安装MongoDB并设计数据结构来存储商品

  • 数据库启动并允许后,我们将实现监听合约时间的NodeJS服务端代码,并将请求记录到控制台,然后我们将执行代码将商品插入数据库中。

  • 我们将更新到我们的前端,从数据库而不是区块链中查找商品(如何保证数据库中的数据不被篡改?)

  • 我们将实现托管合同和相应的前端,参与者可以向买方/卖方发放或退款。

二.初始化项目环境

1.Truffle初识与安装

(1) Truffle简介

Truffle是针对基于以太坊的Solidity语言的一套开发框架。本身基于Javascript,相比于没有框架编写Solidity智能合约,Truffle提供了如下功能

  • 首先对客户端做了深度集成。开发,测试,部署一行命令都可以搞定。不用再记那么多环境地址,繁重的配置更改,及记住诸多的命令。
  • 它提供了一套类似mavengradle这样的项目构建机制,能自动生成相关目录,默认是基于Web的。
  • 简化开发流程:提供了合约抽象接口,可以直接通过合约.deployed()方法拿到合约对象,在Javascript中直接操作对应的合约函数。原理是使用了基于web3.js封装的Ether Pudding工具包。
  • 提供了控制台,使用框架构建后,可以直接在命令行调用输出结果,可极大方便开发调试(这一点有点不敢过于恭维,不少时候在调试的时候还不如Remix)
  • 提供了监控合约,配置变化的自动发布,部署流程。不用每个修改后都重走整个流程。

关于其相关介绍,可以直接到Truffle官网进行了解。

(2) Truffle安装

安装Truffle非常简单,官网上面也非常简单明了

$ npm install truffle -g

同样的,本文只写相关相关的内容与步骤,此处不做过多扩展,移步官方文档查看更多的内容。

2.创建项目目录

$ mkdir auctionDapp/ ; cd auctionDapp
$ truffle unbox webpack

创建项目目录`auctionDapp,并进行初始化工作,返回如下信息则表示truffle项目框架搭建完毕

以太坊Dapp项目-拍卖网站-智能合约编写测试

.
├── LICENSE
├── app  //前端设计
├── box-img-lg.png
├── box-img-sm.png
├── build //智能合约编译后文件存储路径
├── contracts //智能合约文件存储路径
├── migrations //存放发布脚本文件
├── node_modules //相关nodejs库文件
├── package-lock.json 
├── package.json //安装包信息配置文件
├── test //合约测试文件存放路径
├── truffle.js // truffle配置文件
└── webpack.config.js // webpack配置文件

将用于测试的智能合约删除,避免干扰我们的项目。

$ rm -rf contracts/{ConvertLib.sol,MetaCoin.sol}

(1) Truffle Box用途

提到Box,作为蓝鲸智云的忠实粉丝与早期布道者,有必要提一下蓝鲸MagicBox,那是一个专门提供给运维开发人员的前端框架集合,这里的box也是类似的用途,官网是这么描述的

TRUFFLE BOXES
THE EASIEST WAY TO GET STARTED
Truffle Boxes are helpful boilerplates that allow you to focus on what makes your dapp unique. In addition to Truffle, Truffle Boxes can contain other helpful modules, Solidity contracts & libraries, front-end views and more; all the way up to complete example dapps.

简而言之,TRUFFLE BOXES就是将solidity智能合约,相关库,前端框架都集成在一起的集合,方便开发人员在最大程度上简化不必要的环境搭建与技术选型工作。

以太坊Dapp项目-拍卖网站-智能合约编写测试

(2) Webpack框架

Webpack 是一个前端资源加载/打包工具。它将根据模块的依赖关系进行静态分析,然后将这些模块按照指定的规则生成对应的静态资源。

以太坊Dapp项目-拍卖网站-智能合约编写测试

从图中我们可以看出,Webpack 可以将多种静态资源 js、css等转换成一个静态文件,减少了页面的请求。

三.编写测试智能合约

1.定义结构体

本章节定义了一个名为AuctionStore的合约,定义了枚举变量ProductStatus用于区分商品竞拍的阶段,定义枚举变量ProductCondition用于标识拍卖商品是新品还是二手商品,为了便于统计商品数量,我们定义了uint类型变量productIndex通过递增的方式存储商品数量,在商品发布之后会形成两个字典表。

  • 产品Id与钱包地址对应表productIdInStore(多对一)
产品ID 发布者钱包地址
1 0x627306090abab3a6e1400e9345bc60c78a8bef57
2 0xf17f52151ebef6c7334fad080c5704d77216b732
3 0xf17f52151ebef6c7334fad080c5704d77216b732
4 0x627306090abab3a6e1400e9345bc60c78a8bef57
5 0xc5fdf4076b8f3a5357c5e395ab970b5b54098fef

如上表

产品ID(1,4)的发布者为0x627306090abab3a6e1400e9345bc60c78a8bef57

产品ID(2,3)的发布者为0xf17f52151ebef6c7334fad080c5704d77216b732

产品ID为5的发布者为0xc5fdf4076b8f3a5357c5e395ab970b5b54098fef

  • 钱包地址与商品对应表(一对多)stores
发布者钱包地址 产品ID 商品对象
0x627306090abab3a6e1400e9345bc60c78a8bef57 1 如"Macbook Pro 2016"
0x627306090abab3a6e1400e9345bc60c78a8bef57 4 如"IPhone 8 Plus"
0xf17f52151ebef6c7334fad080c5704d77216b732 2 如"IPhone X"
0xf17f52151ebef6c7334fad080c5704d77216b732 3 如"Macbook Pro 2017"
0xc5fdf4076b8f3a5357c5e395ab970b5b54098fef 5 如"Surface Pro4"

代码中定义了投标人结构体Bid,主要保存其投标人钱包地址竞标的产品ID竞标价(虚价)是否揭标,并将其字典映射作为属性放入商品结构体Product中,关于商品结构体Product的相关说明参考代码中注释即可。

pragma solidity ^0.4.24;
//定义合约AuctionStore
contract AuctionStore {
    //定义枚举ProductStatus
    enum ProductStatus {
        Open, //拍卖开始
        Sold, //已售出,交易成功
        Unsold //为售出,交易未成功
    }
    enum ProductCondition {
        New, //拍卖商品是否为新品
        Used //拍卖商品是否已经使用过
    }
    // 用于统计商品数量,作为ID
    uint public productIndex; 
    //产品Id与钱包地址的对应关系
    mapping(uint => address) productIdInStore;
    // 通过地址查找到对应的商品集合
    mapping(address => mapping(uint => Product)) stores;

        //增加投标人信息
    struct Bid {
        address bidder;
        uint productId;
        uint value;
        bool revealed; //是否已经揭标
    }
    struct Product {
        uint id;                 //产品id
        string name;             //商品名称
        string category ;       //商品分类
        string imageLink ;       //图片Hash
        string descLink;        // 图片描述信息的Hash
        uint auctionStartTime; //开始竞标时间
        uint auctionEndTime;    //竞标结束时间
        uint startPrice;       //拍卖价格   
        address highestBidder ; //出价最高,赢家的钱包地址
        uint highestBid ;       //赢家得标的价格
        uint secondHighestBid ; //竞标价格第二名
        uint totalBids ;        //共计竞标的人数
        ProductStatus status;    //状态
        ProductCondition condition ;  //商品新旧标识
        mapping(address => mapping(bytes32 => Bid)) bids;// 存储所有投标人信息

    }
    constructor ()public{
        productIndex = 0;
    }
    }

2. 实现添加商品

我们开始实现拍卖商品的发布操作,需要保证传入的商品拍卖开始时间不能晚于结束时间,当商品被添加后,统计商品的索引ID自增,根据传入的商品属性创建商品product对象,将该对象存入stores中,发布者钱包地址为msg.sender(可以通过from参数传入), 产品ID为当前productIndex的值,同时将数据存入productIdInStore中,Index为当前productIndex的值,Valuemsg.sender,通过该方法可以实现

  • productIndex自增1
  • productIdInStore添加数据
  • stores添加数据
 //实现添加商品到区块链
    function addProductToStore(string _name, string _category, string _imageLink, string _descLink, uint _auctionStartTime, uint _auctionEndTime ,uint _startPrice, uint  _productCondition) public  {
        //开始时间需要小于结束时间
        require(_auctionStartTime < _auctionEndTime,"开始时间不能晚于结束时间");
        //商品索引ID自增
        productIndex += 1;
        //product对象稍后直接销毁,类型为memory即可
        Product memory product = Product(productIndex,_name,_category,_imageLink,_descLink,_auctionStartTime,_auctionEndTime,_startPrice,0,0,0,0,ProductStatus.Open,ProductCondition(_productCondition));
        stores[msg.sender][productIndex] = product;
        productIdInStore[productIndex] = msg.sender;   
    }

3. 读取商品信息

在实现对拍卖商品信息进行读取的时候,我们只需要通过其productIdproductIdInStore中获取发布者地址,通过发布者地址bidderproductIdstores中获取到product对象,从而获取该对象的相关属性信息。

//通过产品ID读取商品信息
    function getProduct(uint _productId)  public view returns (uint,string, string,string,string,uint ,uint,uint, ProductStatus, ProductCondition)  {
        Product memory product = stores[productIdInStore[_productId]][_productId];
        return (product.id, product.name,product.category,product.imageLink,product.descLink,product.auctionStartTime,product.auctionEndTime,product.startPrice,product.status,product.condition);
    }

4. 商品投标操作

商品发布好之后,我们需要在规定的时间段内进行商品投标操作,也就是竞标,首先需要满足几个前提

  • 当前时间不能早于商品竞拍开始时间
  • 当前时间不能晚于商品竞拍结束时间
  • 设置的虚拟价格不能低于开标价格

参考读取商品信息getProduct方法,通过竞标方法传入的productId获取到product对象,将Bid对象存入product对象中,其中传入的加密参数bid是通过加密函数对实际竞标价格+揭标密钥进行加密后得到的,同时将竞标人数递增1。

 //投标,传入参数为产品Id以及Hash值(实际竞标价与秘钥词语的组合Hash),需要添加Payable
    function bid(uint _productId, bytes32 _bid) payable public returns (bool) {
        Product storage product = stores[productIdInStore[_productId]][_productId];
        require(now >= product.auctionStartTime, "商品竞拍时间未到,暂未开始,请等待...");
        require(now <= product.auctionEndTime,"商品竞拍已经结束");
        require(msg.value >= product.startPrice,"设置的虚拟价格不能低于开标价格");
        require(product.bids[msg.sender][_bid].bidder == 0); //在提交竞标之前,必须保证bid的值为空
        //将投标人信息进行保存
        product.bids[msg.sender][_bid] = Bid(msg.sender, _productId, msg.value,false);
        //商品投标人数递增
        product.totalBids += 1;
        //返回投标成功
        return true;
    }

5.公告价格揭标

本文提到的价格公告跟揭标属于同一个概念,只是在使用的时候根据语境进行了相应的调整。

在竞标结束后,竞标人需要进行价格公告,核心在于竞标人传入的实际竞标价_amount与揭标密钥_secret的加密Hash值需要与上文的加密Hashbid要一致,否则会找不到对应的钱包地址,同时要保证该账户之前并未进行价格揭标操作。

以太坊Dapp项目-拍卖网站-智能合约编写测试

//公告,揭标方法
    function revealBid(uint _productId, string _amount, string _secret) public {
        //确保当前时间大于投标结束时间
        require(now > product.auctionEndTime,"竞标尚未结束,未到公告价格时间");
        // 对竞标价格与竞价密钥进行加密
        bytes32 sealedBid = keccak256(_amount,_secret);
        //通过产品ID获取商品信息
        Product storage product = stores[productIdInStore[_productId]][_productId];
        //获取投标人信息
        Bid memory bidInfo = product.bids[msg.sender][sealedBid];
        //判断是否存在钱包地址,钱包地址0x4333  uint160的钱包类型
        require(bidInfo.bidder > 0,"该账户未在竞标者信息中"); 
        //判断该账户是否已经揭标过
        require(bidInfo.revealed == false,"该账户已经揭标");
        // 定义系统的退款
        uint refund;
        uint amount = stringToUint(_amount);
        // bidInfo.value是在竞标时候定义的虚价,通过msg.value设置。
        if (bidInfo.value < amount) { //如果bidInfo.value的值< 实际竞标价,则返回全部退款,属于无效投标
            refund = bidInfo.value;
        }else { //如果属于有效投标,参照如下分类
            if (address(product.highestBidder) == 0) { //第一个参与公告的人,此时该值为0
                //将出标人的地址赋值给最高出标人地址
                product.highestBidder = msg.sender;
                // 将出标人的价格作为最高价格
                product.highestBid = amount;
                // 将商品的起始拍卖价格作为第二高价格
                product.secondHighestBid = product.startPrice;
                // 将多余的钱作为退款,如bidInfo.value = 20,amount = 12,则退款8
                refund = bidInfo.value - amount;
            }else { //此时参与者不是第一个参与公告的人
                // amount = 15 , bidInfo.value = 25,amount > 12 
                if (amount > product.highestBid) {
                    // 将原来的最高价赋值给第二高价
                    product.secondHighestBid = product.highestBid;
                    // 将原来最高的出价退给原先的最高价地址
                    product.highestBidder.transfer(product.highestBid);
                    // 将当前出价者的地址作为最高价地址
                    product.highestBidder = msg.sender;
                    // 将当前出价作为最高价,为15
                    product.highestBid = amount;
                    // 此时退款为 20 - 15 = 5
                    refund = bidInfo.value - amount;
                }else if (amount > product.secondHighestBid) {
                    //将当前竞标价作为第二高价格
                    product.secondHighestBid = amount;
                    //退还所有竞标款
                    refund = amount;
                }else { //如果出价比第二高价还低的话,直接退还竞标款
                    refund = amount;
                }
            }
            if (refund > 0){ //取回退款
                msg.sender.transfer(refund);
                product.bids[msg.sender][sealedBid].revealed = true;
            }
        }

    }

此处的transfer不是常规的转账,可以理解为退款

6.相关帮助方法

    //1. 获取竞标赢家信息
    function highestBidderInfo (uint _productId)public view returns (address, uint ,uint) {
        Product memory product = stores[productIdInStore[_productId]][_productId];
        return (product.highestBidder,product.highestBid,product.secondHighestBid);
    }    
    //2. 获取参与竞标的人数
    function  totalBids(uint _productId) view public returns (uint) {
        Product memory product = stores[productIdInStore[_productId]][_productId];
        return  product.totalBids;
    }
    //3. 将字符串string到uint类型
    function stringToUint(string s) pure private returns (uint) {
        bytes memory b = bytes(s);
        uint result = 0 ;
        for (uint i = 0; i < b.length; i++ ){
            if (b[i] >=48 && b[i] <=57){
                result = result * 10  + (uint(b[i]) - 48);
            }
        }
        return result;
    }

7.合约完整代码

pragma solidity ^0.4.24;
//定义合约AuctionStore
contract AuctionStore {
    //定义枚举ProductStatus
    enum ProductStatus {
        Open, //拍卖开始
        Sold, //已售出,交易成功
        Unsold //为售出,交易未成功
    }
    enum ProductCondition {
        New, //拍卖商品是否为新品
        Used //拍卖商品是否已经使用过
    }
    // 用于统计商品数量,作为ID
    uint public productIndex; 
    //商品Id与钱包地址的对应关系
    mapping(uint => address) productIdInStore;
    // 通过地址查找到对应的商品集合
    mapping(address => mapping(uint => Product)) stores;

    //增加投标人信息
    struct Bid {
        address bidder;
        uint productId;
        uint value;
        bool revealed; //是否已经揭标
    }

    //定义商品结构体
    struct Product {
        uint id;                 //商品id
        string name;             //商品名称
        string category ;       //商品分类
        string imageLink ;       //图片Hash
        string descLink;        // 图片描述信息的Hash
        uint auctionStartTime; //开始竞标时间
        uint auctionEndTime;    //竞标结束时间
        uint startPrice;       //拍卖价格   
        address highestBidder ; //出价最高,赢家的钱包地址
        uint highestBid ;       //赢家得标的价格
        uint secondHighestBid ; //竞标价格第二名
        uint totalBids ;        //共计竞标的人数
        ProductStatus status;    //状态
        ProductCondition condition ;  //商品新旧标识
        mapping(address => mapping(bytes32 => Bid)) bids;// 存储所有投标人信息

    }
    constructor ()public{
        productIndex = 0;
    }
    //添加商品到区块链中
    function addProductToStore(string _name, string _category, string _imageLink, string _descLink, uint _auctionStartTime, uint _auctionEndTime ,uint _startPrice, uint  _productCondition) public  {
        //开始时间需要小于结束时间
        require(_auctionStartTime < _auctionEndTime,"开始时间不能晚于结束时间");
        //商品ID自增
        productIndex += 1;
        //product对象稍后直接销毁即可
        Product memory product = Product(productIndex,_name,_category,_imageLink,_descLink,_auctionStartTime,_auctionEndTime,_startPrice,0,0,0,0,ProductStatus.Open,ProductCondition(_productCondition));
        stores[msg.sender][productIndex] = product;
        productIdInStore[productIndex] = msg.sender;   
    }
    //通过商品ID读取商品信息
    function getProduct(uint _productId)  public view returns (uint,string, string,string,string,uint ,uint,uint, ProductStatus, ProductCondition)  {
        Product memory product = stores[productIdInStore[_productId]][_productId];
        return (product.id, product.name,product.category,product.imageLink,product.descLink,product.auctionStartTime,product.auctionEndTime,product.startPrice,product.status,product.condition);
    }
    //投标,传入参数为商品Id以及Hash值(实际竞标价与秘钥词语的组合Hash),需要添加Payable
    function bid(uint _productId, bytes32 _bid) payable public returns (bool) {
        Product storage product = stores[productIdInStore[_productId]][_productId];
        require(now >= product.auctionStartTime, "商品竞拍时间未到,暂未开始,请等待...");
        require(now <= product.auctionEndTime,"商品竞拍已经结束");
        require(msg.value >= product.startPrice,"设置的虚拟价格不能低于开标价格");
        require(product.bids[msg.sender][_bid].bidder == 0); //在提交竞标之前,必须保证bid的值为空
        //将投标人信息进行保存
        product.bids[msg.sender][_bid] = Bid(msg.sender, _productId, msg.value,false);
        //商品投标人数递增
        product.totalBids += 1;
        //返回投标成功
        return true;
    }

    //公告,揭标方法
    function revealBid(uint _productId, string _amount, string _secret) public {
        //通过商品ID获取商品信息
        Product storage product = stores[productIdInStore[_productId]][_productId];
        //确保当前时间大于投标结束时间
        require(now > product.auctionEndTime,"竞标尚未结束,未到公告价格时间");
        // 对竞标价格与关键字密钥进行加密
        bytes32 sealedBid = keccak256(_amount,_secret);
        //获取投标人信息
        Bid memory bidInfo = product.bids[msg.sender][sealedBid];
        //判断是否存在钱包地址,钱包地址0x4333  uint160的钱包类型
        require(bidInfo.bidder > 0,"钱包地址不存在"); 
        //判断是否已经公告揭标过
        require(bidInfo.revealed == false,"已经揭标");
        // 定义系统的退款
        uint refund;
        uint amount = stringToUint(_amount);
        // bidInfo.value 其实就是 mask bid,用于迷惑竞争对手的价格
        if (bidInfo.value < amount) { //如果bidInfo.value的值< 实际竞标价,则返回全部退款,属于无效投标
            refund = bidInfo.value;
        }else { //如果属于有效投标,参照如下分类
            if (address(product.highestBidder) == 0) { //第一个参与公告的人,此时该值为0
                //将出标人的地址赋值给最高出标人地址
                product.highestBidder = msg.sender;
                // 将出标人的价格作为最高价格
                product.highestBid = amount;
                // 将商品的起始拍卖价格作为第二高价格
                product.secondHighestBid = product.startPrice;
                // 将多余的钱作为退款,如bidInfo.value = 20,amount = 12,则退款8
                refund = bidInfo.value - amount;
            }else { //此时参与者不是第一个参与公告的人
                // amount = 15 , bidInfo.value = 25,amount > 12 
                if (amount > product.highestBid) {
                    // 将原来的最高价地址 赋值给 第二高价的地址
                    product.secondHighestBid = product.highestBid;
                    // 将原来最高的出价退还给原先退给原先的最高价地址
                    product.highestBidder.transfer(product.highestBid);
                    // 将当前出价者的地址作为最高价地址
                    product.highestBidder = msg.sender;
                    // 将当前出价作为最高价,为15
                    product.highestBid = amount;
                    // 此时退款为 20 - 15 = 5
                    refund = bidInfo.value - amount;
                }else if (amount > product.secondHighestBid) {
                    //
                    product.secondHighestBid = amount;
                    //退还所有竞标款
                    refund = amount;
                }else { //如果出价比第二高价还低的话,直接退还竞标款
                    refund = amount;
                }
            }
            if (refund > 0){ //退款
                msg.sender.transfer(refund);
                product.bids[msg.sender][sealedBid].revealed = true;
            }
        }

    }

    //帮助方法
    //1. 获取竞标赢家信息
    function highestBidderInfo (uint _productId)public view returns (address, uint ,uint) {
        Product memory product = stores[productIdInStore[_productId]][_productId];
        return (product.highestBidder,product.highestBid,product.secondHighestBid);
    }    
    //2. 获取参与竞标的人数
    function  totalBids(uint _productId) view public returns (uint) {
        Product memory product = stores[productIdInStore[_productId]][_productId];
        return  product.totalBids;
    }
    //3. 将字符串string到uint类型
    function stringToUint(string s) pure private returns (uint) {
        bytes memory b = bytes(s);
        uint result = 0 ;
        for (uint i = 0; i < b.length; i++ ){
            if (b[i] >=48 && b[i] <=57){
                result = result * 10  + (uint(b[i]) - 48);
            }
        }
        return result;
    }
} 

8.合约测试

(1) 启动测试终端

$ truffle  develop

以太坊Dapp项目-拍卖网站-智能合约编写测试

(2) 编译合约

以太坊Dapp项目-拍卖网站-智能合约编写测试

此处Warning警告信息忽略即。

(3) 部署合约

以太坊Dapp项目-拍卖网站-智能合约编写测试

(4) 安装依赖库

安装ethereumjs-util,加密方法需要调用该库

$ npm install ethereumjs-util

(5) 查询测试账户

truffle(develop)> web3.eth.getBalance(web3.eth.accounts[1])
BigNumber { s: 1, e: 20, c: [ 1000000 ] }
truffle(develop)> web3.eth.getBalance(web3.eth.accounts[2])
BigNumber { s: 1, e: 20, c: [ 1000000 ] 
truffle(develop)> web3.eth.getBalance(web3.eth.accounts[3])
BigNumber { s: 1, e: 20, c: [ 1000000 ] 

查询用于测试的账户(竞标账户)的原始额度,均为100000.

(6) 商品发布

  • 初始化竞标价格
truffle(develop)> auctionAmount = web3.toWei(1,'ether')
'1000000000000000000'
  • 获取当前时间
truffle(develop)> auctionStartTime = Math.round(new Date() / 1000);  
1539885333
  • 调用发布合约
truffle(develop)> AuctionStore.deployed().then(function(i) {i.addProductToStore('Macbook Pro 2018 001','Phones &  Computers','imagesLink','descLink',auctionStartTime,auctionStartTime + 300,auctionAmount,0).then(function(f) {console.log(f)})});

竞标时间设置为5分钟

以太坊Dapp项目-拍卖网站-智能合约编写测试

(7) 查看相关参数

  • 查看商品个数
truffle(develop)> AuctionStore.deployed().then(function(i) {i.productIndex.call().then(function(f) {console.log(f)})})

以太坊Dapp项目-拍卖网站-智能合约编写测试

  • 查看商品信息
truffle(develop)> AuctionStore.deployed().then(function(i) {i.getProduct.call(1).then(function(f) {console.log(f)})})

以太坊Dapp项目-拍卖网站-智能合约编写测试

获取合约的方式还有:

truffle(develop)>var instance

truffle(develop)> instance = AuctionStore.deployed().then((i => {instance = i}))

truffle(develop)> instance.productIndex();

BigNumber { s: 1, e: 0, c: [ 1 ] }

(8) 开始竞标

务必在竞标结束时间前完成竞标操作

  • 对实际出标价与揭标密钥进行加密

[1] 导入加密库

truffle(develop)> EjsUtil = require('ethereumjs-util') 

[2] 进行加密

truffle(develop)> sealedBid1 = '0x' + EjsUtil.keccak256(2*auctionAmount + 'firstsecrt').toString('hex') 
'0xb0d5a0c4d195f138442910cd2ccd16da585784a24482f7e320f48d850e0fb86d'
truffle(develop)> sealedBid2 = '0x' + EjsUtil.keccak256(3*auctionAmount + 'secondsecrt').toString('hex') 
'0x9566873896902aca059cbe402b2aa82638fe6e57980c97ac25c576cc6496a233'
truffle(develop)> sealedBid3 = '0x' + EjsUtil.keccak256(4*auctionAmount + 'threesecrt').toString('hex') 
'0x79e5fcbcc9065408e06f20d224c7183d82089e0fbe8e344446b5f4527b5d2f4f'
  • 账户1参与竞标

实际amount = 2 auctionAmount , Mask BId: 2.5 *auctionAmount

truffle(develop)> AuctionStore.deployed().then(function(i){i.bid(1,sealedBid1,{value:2.5*auctionAmount,from:web3.eth.accounts[1]}).then(function(f) {console.log(f)})})

以太坊Dapp项目-拍卖网站-智能合约编写测试

truffle(develop)> web3.eth.getBalance(web3.eth.accounts[1])
BigNumber { s: 1, e: 19, c: [ 974888, 13600000000000 ] }
  • 账户2参与竞标

实际amount =3 * auctionAmount , Mask BId: 3.5 *auctionAmount

truffle(develop)> AuctionStore.deployed().then(function(i){i.bid(1,sealedBid2,{value:3.5*auctionAmount,from:web3.eth.accounts[2]}).then(function(f) {console.log(f)})})

以太坊Dapp项目-拍卖网站-智能合约编写测试

truffle(develop)> web3.eth.getBalance(web3.eth.accounts[1])
BigNumber { s: 1, e: 19, c: [ 974888, 13600000000000 ] }
truffle(develop)> web3.eth.getBalance(web3.eth.accounts[2])
BigNumber { s: 1, e: 19, c: [ 964903, 13600000000000 ] }
  • 账户3参与竞标

实际amount = 4 * auctionAmount , Mask BId: 4.5 *auctionAmount

truffle(develop)> AuctionStore.deployed().then(function(i){i.bid(1,sealedBid3,{value:4.5*auctionAmount,from:web3.eth.accounts[3]}).then(function(f) {console.log(f)})})

以太坊Dapp项目-拍卖网站-智能合约编写测试

truffle(develop)> web3.eth.getBalance(web3.eth.accounts[1])
BigNumber { s: 1, e: 19, c: [ 974888, 13600000000000 ] } //扣除2.5ether以及部分gas
truffle(develop)> web3.eth.getBalance(web3.eth.accounts[2])
BigNumber { s: 1, e: 19, c: [ 964903, 13600000000000 ] }//扣除3.5ether以及部分gas
truffle(develop)> web3.eth.getBalance(web3.eth.accounts[3])
BigNumber { s: 1, e: 19, c: [ 954903, 13600000000000 ] } //扣除4.5ether以及部分gas

(9) 公告揭标

时间必须超过竞标结束时间才能执行合约,揭标时需要填写实际竞标价

  • 账户1进行揭标
truffle(develop)> AuctionStore.deployed().then(function(i) {i.revealBid(1,(2*auctionAmount).toString(),'firstsecrt',{from: web3.eth.accounts[1]}).then(function(f){console.log(f)})});

以太坊Dapp项目-拍卖网站-智能合约编写测试

truffle(develop)> web3.eth.getBalance(web3.eth.accounts[1])
BigNumber { s: 1, e: 19, c: [ 979711, 68300000000000 ] }//观察变化
truffle(develop)> web3.eth.getBalance(web3.eth.accounts[2])
BigNumber { s: 1, e: 19, c: [ 964903, 13600000000000 ] }
truffle(develop)> web3.eth.getBalance(web3.eth.accounts[3])
BigNumber { s: 1, e: 19, c: [ 954903, 13600000000000 ] }
  • 账户2进行揭标
truffle(develop)> AuctionStore.deployed().then(function(i) {i.revealBid(1,(3*auctionAmount).toString(),'secondsecrt',{from: web3.eth.accounts[2]}).then(function(f){console.log(f)})});

以太坊Dapp项目-拍卖网站-智能合约编写测试

truffle(develop)>  web3.eth.getBalance(web3.eth.accounts[1])
BigNumber { s: 1, e: 19, c: [ 999711, 68300000000000 ] } //观察变化
truffle(develop)> web3.eth.getBalance(web3.eth.accounts[2])
BigNumber { s: 1, e: 19, c: [ 969815, 53800000000000 ] } //观察变化
truffle(develop)> web3.eth.getBalance(web3.eth.accounts[3])
BigNumber { s: 1, e: 19, c: [ 954903, 13600000000000 ] }
  • 账户3进行揭标
truffle(develop)> AuctionStore.deployed().then(function(i) {i.revealBid(1,(4* auctionAmount).toString(),'threesecrt',{from: web3.eth.accounts[3]}).then(function(f){console.log(f)})});

以太坊Dapp项目-拍卖网站-智能合约编写测试

truffle(develop)> web3.eth.getBalance(web3.eth.accounts[1])
BigNumber { s: 1, e: 19, c: [ 999711, 68300000000000 ] }
truffle(develop)> web3.eth.getBalance(web3.eth.accounts[2])
BigNumber { s: 1, e: 19, c: [ 999815, 53800000000000 ] }
truffle(develop)> web3.eth.getBalance(web3.eth.accounts[3])
BigNumber { s: 1, e: 19, c: [ 959815, 60200000000000 ] }

(10) 查看赢家信息

truffle(develop)> AuctionStore.deployed().then(function(i){i.highestBidderInfo.call(1).then(function(f){console.log(f)})});

以太坊Dapp项目-拍卖网站-智能合约编写测试

9.余额变化表

操作 账户1 账户2 账户3
初始余额 10 10 10
开始竞标 - - -
实际竞标价格 2 3 4
对外虚拟价格 2.5 3.5 4.5
账户余额 9.74888 9.64903 9.54903
账户1开始揭标 - - -
揭标结果 最高价(退款为2.5-2) - -
揭标余额 9.79711 9.64903 9.54903
账户2开始揭标 - - -
揭标结果 出局(退款为实际竞标价2) 最高价(退款为2.5-2) -
揭标余额 9.99711 969815 -
账户3开始揭标 - - -
揭标结果 出局(不变) 出局(退款为实际竞标价3) 最高价(退款为4.5-4)
揭标余额 9.99711 9.99815 9.59815

由于时间问题,本文先介绍拍卖网站的智能合约部分,其他内容会根据后续时间安排考虑再完善,感谢理解与支持!

猜你喜欢

转载自blog.51cto.com/clovemfong/2306741
今日推荐