1. 程式人生 > >以太坊Dapp項目-拍賣網站-智能合約編寫測試

以太坊Dapp項目-拍賣網站-智能合約編寫測試

添加 unbox 基於 文檔 找不到 文件的 align 保存 returns

修訂日期 姓名 郵箱 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

由於時間問題,本文先介紹拍賣網站的智能合約部分,其他內容會根據後續時間安排考慮再完善,感謝理解與支持!

以太坊Dapp項目-拍賣網站-智能合約編寫測試