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

修訂日期 姓名 郵箱
2018-10-18 brucefeng brucefeng@brucefeng.com

前言

寫這篇文章的初衷其實很簡單,在MyEtherWallet上申請以太坊ENS的時候,競標的以太幣兩次被吞,並且是在規定時間點進行了價格公告,這篇文章的設計思路其實就是跟ENS的競標流程相似,但願對你們有所幫助,因此,準備寫完以後,再從新去整一次ENS的申請,若是再被吞,我就要舉報了:-),本文主要是本人用於項目整理,便於本身查詢,不作任何商業用途。css

如今迴歸到技術上來,這個項目其實涉及到蠻多的知識點的,是很是不錯的以太坊智能合約以及Dapp學習項目,至少在目前而言,尚未看到特別好的學習項目被分享出來,經過該項目,咱們能夠掌握以下內容:前端

  • 以太坊智能合約編程語言Solidity的編寫
  • 智能合約框架Truffle的學習與使用
  • 以太坊與IPFS的整合
  • NodeJS編程學習
  • 以太坊Web3JS的接口學習
  • Dapp與主流數據庫的整合(本文爲NoSQL類型的MongoDB)
  • 維克裏拍賣法則

一.項目介紹

1.項目功能

(1)項目展現node

容許商家列出項目,咱們將爲任何人創建免費列出項目的功能,咱們會將這些項目都存儲在區塊鏈和非區塊鏈的數據庫中,方便查詢。webpack

(2) 文件存儲web

將文件添加到IPFS:咱們將商品圖像和商品描述(大文本)上傳至IPFS的功能。mongodb

(3)瀏覽商品數據庫

咱們將添加根據類別,拍賣時間等過濾和瀏覽商品的功能。npm

(4)商品拍賣編程

實現維克裏密封拍賣,招標流程跟ENS相似。json

(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

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

相關文章
相關標籤/搜索