以太坊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.项目架构

以下图片来源于网络

(1) Web前端

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

(2) 区块链

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

(3) NodeJS服务器

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

(4) MongoDB

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

(5)区块链存储IPFS

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

3. 业务流向

(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项目框架搭建完毕

.
├── 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智能合约,相关库,前端框架都集成在一起的集合,方便开发人员在最大程度上简化不必要的环境搭建与技术选型工作。

(2) Webpack框架

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

从图中我们可以看出,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要一致,否则会找不到对应的钱包地址,同时要保证该账户之前并未进行价格揭标操作。

//公告,揭标方法
    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

(2) 编译合约

此处Warning警告信息忽略即。

(3) 部署合约

(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分钟

(7) 查看相关参数

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

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

获取合约的方式还有:

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)})})

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)})})

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)})})

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)})});

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)})});

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)})});

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)})});

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

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

原文地址:http://blog.51cto.com/clovemfong/2306741

时间: 2024-10-01 10:55:10

以太坊Dapp项目-拍卖网站-智能合约编写测试的相关文章

以太坊Dapp项目-网页钱包开发手册

以太坊Dapp项目-网页钱包开发手册 修订日期 姓名 邮箱 2018-10-10 brucefeng [email protected] 前言 在之前的一篇文章以太坊智能合约项目-Token合约开发与部署中,我们提到了钱包了钱包的概念以及主流的几种钱包,如Mist,MyEtherWallet,MetaMask等,之前我们主要将钱包作为一个开发工具使用,用于智能合约的开发与调试工作,使用较多的是浏览器插件钱包MetaMask. 在本文中,我们主要介绍MyEtherWallet以及如何实现一个简易版

以太坊 DApp 开发入门实战! 用Node.js和truffle框架搭建——区块链投票系统!

第一节 概述 面向初学者,内容涵盖以太坊开发相关的基本概念,并将手把手地教大家如何构建一个 基于以太坊的完整去中心化应用 -- 区块链投票系统. 通过学习,你将掌握: 以太坊区块链的基本知识 开发和部署以太坊合约所需的软件环境 使用高级语言(solidity)编写以太坊合约 使用NodeJS编译.部署合约并与之交互 使用Truffle框架开发分布式应用 使用控制台或网页与合约进行交互 前序知识要求 为了顺利完成,最好对以下技术已经有一些基本了解: 一种面向对象的开发语言,例如:Python,Ru

区块链学习(3)--以太坊Dapp开发

DApp是Decentralized Application的缩写,译为:分散式的应用程序.App我们都知道,我们在智能手机上安装的应用程序也就是App.而DApp比App多了一个'D','D'的意思是分散式的.意思是 分散式的应用程序/去中心化的应用程序.与传统的App最大的区别是:DApp运行在去中心化的网络上,也就是区块链网络中.这里的DApp开发用以太坊智能合约为例,智能合约是记录在链上的一段能够控制链行为事件的一段协议,如:合约下关联账户转币.查账.投票.购买等等,合约里涉及的变量.常

2018年以太坊智能合约开发语言Solidity最佳IDEs

Solidity是一种以智能合约为导向的编程语言.这是一种只有四年的年轻语言,旨在帮助开发基于以太坊数字货币的智能合约. 理解它官方文档应该是学习Solidity的最佳来源:solidity.readthedocs.io 想在以太坊的网络上建立自己的加密货币吗?想拥有自己的初始代码产品吗?以下是您今天可以使用的最佳Solidity IDE. Remix IDE Remix IDE是基于浏览器的,并且集成了编译器.没有服务器端组件. 官网: https://remix.ethereum.orggi

第18讲 | 智能合约与以太坊

在前面的文章里,我们介绍了区块链的核心技术,也穿插介绍了一些项目.然而每个区块链都有自己的特色,接下来我们将针对每个项目进行详细讲解.今天我们就来讲讲智能合约和以太坊项目. 今天我们从智能合约这个概念入手,聊聊什么是以太坊项目以及它的发展历史.最后还会介绍几款钱包给你,希望通过今天文章的讲解,你也可以尝试在以太坊上编写简单的智能合约. 智能合约的概念 不同于法律意义上的合约概念,区块链领域的合约表达的是可以“自治自理”的 计算机协议,这套协议具有自我执行.自我验证的属性. 如果完全从技术角度来看

python如何使用web3py与以太坊投资智能合约交互

在以太坊和其他区块链中,仍有很多被证明的概念正在实施,开发人员在尝试如何应对这些新概念.作为dInvest 系列文章一部分,我也在研究以太坊并尝试在区块链中实施对冲基金.在上一篇文章中,我讨论了如何在python中启动和运行定量框架.在这篇文章中,我将介绍如何将python程序与以太坊智能合约集成.出于这样或那样的原因,可能也面临着这个问题,尽管以太坊提供了图灵完备语言,但并不是所有事情都能完成. 假设你已经在以太坊创建了一个简单的教程合约,现在想要看一些更高级的东西.我个人喜欢ManuelAr

第一行代码:以太坊(2)-使用Solidity语言开发和测试智能合约

智能合约是以太坊的核心之一,用户可以利用智能合约实现更灵活的代币以及其他DApp.不过在深入讲解如何开发智能合约之前,需要先介绍一下以太坊中用于开发智能合约的Solidity语言,以及相关的开发和测试环境. 智能合约就是运行在以太坊上的程序.客户端可以通过Web3.js API调用智能合约,而智能合约本身又可以直接访问以太坊网络,也就是说,智能合约前面连接着客户端,后面连接着以太坊网络,起到了承前启后的作用,而且通过智能合约,可以让整个以太坊网络更灵活,可控性更强.其实智能合约的作用相当于微软O

win7下以太坊基于truffle+ganache开发环境搭建以及智能合约的部署调用

上一篇介绍的是以太坊下基于geth+remix-ide智能合约环境的搭建和部署运行,本篇介绍的是基于truffle+ganache. ganache相当于是geth的图形化操作界面,相对于纯指令操作的geth较为简单易上手,并且运行交易和生成区块的过程一目了然. [前期准备] 1.Node.js安装(这一点在上一篇文章中提到过,所以此处不做展示) 2.指令输入: npm install -g solc(安装智能合约) npm install -g ganache-cli (安装ganache开发

如何从零开始学习区块链技术——推荐从以太坊开发DApp开始

很多人迷惑于区块链和以太坊,不知如何学习,本文简单说了一下学习的一些方法和资源. 一. 以太坊和区块链的关系 从区块链历史上来说,先诞生了比特币,当时并没有区块链这个技术和名词,然后业界从比特币中提取了技术架构和体系,称之为区块链技术.从比特币提取的区块链技术称之为区块链1.0时代,那个时候的应用主要以电子货币和去中心化交易为主,比如各种山寨币.而以太坊将区块链带入了2.0的时代,区块链2.0不是推翻了1.0,而是在1.0的基础上实现了区块知晓.价值知晓.图灵完备,并进行了细节优化,从而形成了以