1. 程式人生 > >以太坊Dapp專案-拍賣網站-智慧合約編寫測試

以太坊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

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