以太坊構建DApps系列教程(七):爲DAO合約構建Web3 UI

在本系列關於使用以太坊構建DApps教程的第6部分中,咱們經過添加投票,黑名單,股息分配和撤銷來完成DAO合約,同時投入一些額外的輔助函數以實現良好的標準。在本教程中,咱們將構建一個用於與咱們的故事Story交互的Web界面,不然咱們沒法統計用戶如何參與。因此這是咱們故事Story發佈以前的最後一部分。javascript

因爲這不是一個Web應用程序教程,咱們將保持很是簡單。下面的代碼不是生產就緒的,只是做爲如何將JavaScript鏈接到區塊鏈的概念證實。但首先,讓咱們添加一個新的遷移。php

自動轉移

如今,當咱們部署代幣和DAO時,它們位於區塊鏈上但不進行交互。爲了測試咱們構建的內容,咱們須要手動將代幣全部權和餘額轉移到DAO,這在測試期間可能很乏味。css

讓咱們寫一個新的遷移,爲咱們作這件事。建立文件4_configure_relationship.js並將如下內容放在其中:html

var Migrations = artifacts.require("./Migrations.sol");
var StoryDao = artifacts.require("./StoryDao.sol");
var TNSToken = artifacts.require("./TNSToken.sol");

var storyInstance, tokenInstance;

module.exports = function (deployer, network, accounts) {

    deployer.then(function () {
            return TNSToken.deployed();
        }).then(function (tIns) {
            tokenInstance = tIns;
            return StoryDao.deployed();
        }).then(function (sIns) {
            storyInstance = sIns;
            return balance = tokenInstance.totalSupply();
        }).then(function (bal) {
            return tokenInstance.transfer(storyInstance.address, bal);
        })
        .then(function (something) {
            return tokenInstance.transferOwnership(storyInstance.address);
        });
}

這是這段代碼的做用。首先,你會注意到它是基於promise的。它充滿了各類調用。這是由於咱們在調用下一個數據以前依賴於返回一些數據的函數。全部合約調用都是基於promise的,這意味着它們不會當即返回數據,由於Truffle須要向節點請求信息,所以promise在未來返回數據。咱們強制代碼等待這些數據,使用then關鍵詞並提供全部then調用函數,這些函數在最終給出時將使用此結果調用。前端

因此,按順序:java

  • 首先,向節點詢問已部署代幣的地址並將其返回。
  • 而後,接受此數據,將其保存到全局變量中,並詢問已部署的DAO的地址並將其返回。
  • 而後,接受這些數據,將其保存到全局變量中,並詢問代幣合約的全部者將在其賬戶中具備的餘額,這在技術上是總供應量,並返回此數據。
  • 而後,一旦你獲得這個餘額,用它來調用這個代幣的transfer函數,並將令牌發送到DAO的地址並返回結果。
  • 而後,忽略返回的結果——咱們只想知道它什麼時候完成——最後將代幣的全部權轉移到DAO的地址,返回數據但不丟棄它。

運行truffle migrate --reset如今應該產生這樣的輸出:node

前端

前端是一個常規的靜態HTML頁面,其中包含一些JavaScript用於與區塊鏈和一些CSS進行通訊以使頁面變得不那麼難看。python

讓咱們在子文件夾public建立一個文件index.html,併爲其提供如下內容:android

<!DOCTYPE HTML>

<html lang="en">
<head>
    <title>The Neverending Story</title>
    <meta charset="utf-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1"/>
    <meta name="description" content="The Neverending Story is an community curated and moderated Ethereum dapp-story">
    <link rel="stylesheet" href="assets/css/main.css"/>
</head>
<body>

    <div class="grid-container">
        <div class="header container">
            <h1>The Neverending Story</h1>
            <p>A story on the Ethereum blockchain, community curated and moderated through a Decentralized Autonomous Organization (DAO)</p>
        </div>
        <div class="content container">
            <div class="intro">
                <h3>Chapter 0</h3>
                <p class="intro">It's a rainy night in central London.</p>
            </div>
            <hr>
            <div class="content-submissions">
                <div class="submission">
                    <div class="submission-body">This is an example submission. A proposal for its deletion has been submitted.</div>
                    <div class="submission-submitter">0xbE2B28F870336B4eAA0aCc73cE02757fcC428dC9</div>
                    <div class="submission-actions">
                        <div class="deletionproposed" data-votes="3024" data-deadline="1531607200"></div>
                    </div>
                </div>
                <div class="submission">
                        <div class="submission-body">This is a long submission. It has over 244 characters, just we can see what it looks like when rendered in the UI. We need to make sure it doesn't break anything and the layout also needs to be maintained, not clashing with actions/buttons etc.</div>
                        <div class="submission-submitter">0xbE2B28F870336B4eAA0aCc73cE02757fcC428dC9</div>
                        <div class="submission-actions">
                            <div class="delete"></div>
                        </div>
                </div>
                <div class="submission">
                        <div class="submission-body">This is an example submission. A proposal for its deletion has been submitted but is looking like it'll be rejected.</div>
                        <div class="submission-submitter">0xbE2B28F870336B4eAA0aCc73cE02757fcC428dC9</div>
                        <div class="submission-actions">
                            <div class="deletionproposed" data-votes="-790024" data-deadline="1531607200"></div>
                        </div>
                </div>
            </div>    
        </div>
        <div class="events container">
            <h3>Latest Events</h3>
            <ul class="eventlist">

            </ul>
        </div>
        <div class="information container">
            <p>Logged in / out</p>
            <div class="avatar">
                <img src="http://placeholder.pics/svg/200/DEDEDE/555555/avatar" alt="avatar">
            </div>
            <dl>
                <dt>Contributions</dt>
                <dd>0</dd>
                <dt>Deletions</dt>
                <dd>0</dd>
                <dt>Tokens</dt>
                <dd>0</dd>
                <dt>Proposals submitted</dt>
                <dd>0</dd>
                <dt>Proposals voted on</dt>
                <dd>0</dd>
            </dl>
        </div>
    </div>

<script src="assets/js/web3.min.js"></script>
<script src="assets/js/app.js"></script>
<script src="assets/js/main.js"></script>

</body>
</html>

注意:這是一個很是基本的框架,僅用於演示集成。請不要把這當成最終產品!程序員

你可能缺乏web3文件夾中的dist文件夾。該軟件仍處於測試階段,所以仍有可能出現輕微漏洞。要解決此問題並使用dist文件夾,請運行npm install ethereum/web3.js --save

對於CSS,讓咱們在public/assets/css/main.css一些基本內容:

@supports (grid-area: auto) {
    .grid-container{
      display: grid;
      grid-template-columns: 6fr 5fr 4fr;
      grid-template-rows: 10rem ;
      grid-column-gap: 0.5rem;
      grid-row-gap: 0.5rem;
      justify-items: stretch;
      align-items: stretch;
      grid-template-areas:
      "header header information"
      "content events information";
      height: 100vh;
    }
    .events {
      grid-area: events;
    }
    .content {
      grid-area: content;
    }
    .information {
      grid-area: information;
    }
    .header {
      grid-area: header;
      text-align: center;
    }

    .container {
        border: 1px solid black;
        padding: 15px;
        overflow-y: scroll;
    }

    p {
        margin: 0;
    }
  }

body {
    padding: 0;
    margin: 0;
    font-family: sans-serif;
}

而後做爲JS,咱們將在public/assets/js/app.js

var Web3 = require('web3');

var web3 = new Web3(web3.currentProvider);
console.log(web3);

這裏發生了什麼?

既然咱們假設全部用戶都安裝了MetaMask,而且MetaMask將本身的Web3實例注入到任何訪問過的網頁的DOM中,咱們基本上能夠訪問咱們網站上MetaMask的wallet provider。實際上,若是咱們在頁面打開時登陸MetaMask,咱們將在控制檯中看到:

注意MetamaskInpageProvider是如何激活的。實際上,若是咱們在控制檯中鍵入web3.eth.accounts,咱們將經過MetaMask訪問的全部賬戶都將打印出來:

可是,這個特殊賬戶默認添加到我本身的我的Metamask中,所以餘額爲0eth。它不是咱們運行的Ganache或PoA鏈的一部分:

請注意,若是要求咱們的MetaMask活動賬戶的餘額產生0,同時要求咱們的一個私有區塊鏈賬戶的餘額產生100以太(在個人狀況下它是Ganache,因此全部賬戶都用100以太初始化)。

關於語法

你會注意到這些調用的語法看起來有點奇怪:

web3.eth.getBalance("0x35d4dCDdB728CeBF80F748be65bf84C776B0Fbaf", function(err, res){console.log(JSON.stringify(res));});

爲了讀取區塊鏈數據,大多數MetaMask用戶不會在本地運行節點,而是從Infura或其餘遠程節點請求它。所以,咱們實際上能夠依靠回調。所以,一般不支持同步方法。相反,一切都是經過promise或回調來完成的——就像本文開頭的部署步驟同樣。這是否意味着你須要很是熟悉爲以太坊開發JS的promise?不,這意味着如下內容。在DOM中進行JS調用時......

  • 老是提供一個回調函數做爲你正在調用的函數的最後一個參數。
  • 假設它的返回值是雙重的:第一個error,而後是result

因此,基本上,只需考慮延遲響應就能夠了。當節點響應數據時,你定義爲回調函數的函數將由JavaScript調用。是的,這意味着你不能期望你的代碼在編寫時逐行執行!

有關promises,回調和全部async jazz的更多信息,請參閱此文章

賬戶信息

若是咱們打開上面提到的網站骨架,咱們獲得這樣的東西:

讓咱們用真實數據填充關於賬戶信息的最右側列。

session

當用戶未登陸其MetaMask擴展名時,賬戶列表將爲空。若是甚至沒有安裝MetaMask,則提供程序將爲空(未定義)。當他們登陸MetaMask時,接口將可用並提供賬戶信息以及與鏈接的以太坊節點(live或Ganache或其餘)的交互。

提示:要進行測試,你能夠經過單擊右上角的頭像圖標而後選擇註銷來註銷MetaMask。若是用戶界面看起來不像下面的屏幕截圖,你可能須要經過打開菜單並單擊「試用Beta」來激活Beta用戶界面。

首先,若是用戶已註銷,請將該狀態列的全部內容替換爲用戶的消息:

<div class="information container">
    <div class="logged out">
        <p>You seem to be logged out of MetaMask or MetaMask isn't installed. Please log into MetaMask - to learn more,
            see
            <a href="https://bitfalls.com/2018/02/16/metamask-send-receive-ether/">this tutorial</a>.</p>
    </div>
    <div class="logged in" style="display: none">
        <p>You are logged in!</p>
    </div>
</div>

處理它的JS看起來像這樣(在public/assets/js/main.js):

var loggedIn;

(function () {

    loggedIn = setLoggedIn(web3.currentProvider !== undefined && web3.eth.accounts.length > 0);

})();

function setLoggedIn(isLoggedIn) {
    let loggedInEl = document.querySelector('div.logged.in');
    let loggedOutEl = document.querySelector('div.logged.out');

    if (isLoggedIn) {
        loggedInEl.style.display = "block";
        loggedOutEl.style.display = "none";
    } else {
        loggedInEl.style.display = "none";
        loggedOutEl.style.display = "block";
    }

    return isLoggedIn;
}

第一部分——(function () { -包含一旦網站加載就要執行的邏輯。所以,當頁面準備就緒時,內部的任何內容都會當即執行。調用單個函數setLoggedIn並將條件傳遞給它條件是:

  • 設置web3對象的currentProvider(即網站中存在web3客戶端)。
  • 可用的賬戶數量非零,便可經過此Web3提供商使用賬戶。 換句話說,咱們已登陸至少一個賬戶。

若是這些條件一塊兒評估爲true,則setLoggedIn函數使「Logged out」消息不可見,而且「Logged In」消息可見。

全部這些都具備可以使用任何其餘web3提供商的額外優點。若是最終出現MetaMask替代方案,它將當即與此代碼兼容,由於咱們並未明確指望任何地方的MetaMask。

賬戶頭像

由於以太坊錢包的每一個私鑰都是惟一的,因此它可用於生成獨特的圖像。這是你在MetaMask的右上角或使用MyEtherWallet時看到的彩色化身,儘管Mist,MyEtherWallet和MetaMask都使用不一樣的方法。讓咱們爲登陸用戶生成一個並顯示它。

Mist中的圖標是使用Blockies庫生成的——是自定義的,由於原始文件具備損壞的隨機數生成器,而且能夠爲不一樣的鍵生成相同的圖像。所以,要安裝此文件,請將此文件下載到assets/js文件夾中。而後,在index.html咱們在main.js以前包含它:

<script src="assets/js/app.js"></script>
    <script src="assets/js/blockies.min.js"></script>
    <script src="assets/js/main.js"></script>

</body>

咱們還應該升級logged.in容器:

<div class="logged in" style="display: none">
    <p>You are logged in!</p>
    <div class="avatar">

    </div>
</div>

在main.js,咱們啓動該功能。

if (isLoggedIn) {
      loggedInEl.style.display = "block";
      loggedOutEl.style.display = "none";

      var icon = blockies.create({ // All options are optional
          seed: web3.eth.accounts[0], // seed used to generate icon data, default: random
          size: 20, // width/height of the icon in blocks, default: 8
          scale: 8, // width/height of each block in pixels, default: 4
      });

      document.querySelector("div.avatar").appendChild(icon);

所以,咱們升級JS代碼的登陸部分以生成圖標並將其粘貼到頭像部分。咱們應該在渲染以前將它與CSS稍微對齊:

div.avatar { width: 100%; text-align: center; margin: 10px 0; }

如今,若是咱們在登陸MetaMask時刷新頁面,咱們應該會看到生成的頭像圖標。

賬戶餘額

如今讓咱們輸出一些賬戶餘額信息。

咱們擁有一系列只讀功能,咱們專門爲此目的而開發。因此讓咱們查詢區塊鏈並詢問一些信息。爲此,咱們須要經過如下步驟調用智能合約功能

1.ABI

獲取咱們正在調用的函數的合約的ABI。ABI包含函數簽名,所以咱們的JS代碼知道如何調用它們。在此處瞭解有關ABI的更多信息。

你能夠經過在編譯後打開項目文件夾中的build/TNSToken.jsonbuild/StoryDao.json文件並僅選擇abi部分來獲取TNS代幣和StoryDAO的ABI([]方括號之間的部分):

咱們將這個ABI放在咱們的JavaScript代碼的頂部,進入main.js以下所示:

請注意,上面的屏幕截圖顯示了個人代碼編輯器(Microsoft Visual Code)摺疊的縮寫插入。若是你查看行號,你會注意到令牌的ABI是400行代碼,而DAO的ABI是另外1000行,因此將它粘貼到本文中是沒有意義的。

2.實例化代幣

if (loggedIn) {

    var token = TNSToken.at('0x3134bcded93e810e1025ee814e87eff252cff422');
    var story = StoryDao.at('0x729400828808bc907f68d9ffdeb317c23d2034d5');
    token.balanceOf(web3.eth.accounts[0], function(error, result) {console.log(JSON.stringify(result))});
    story.getSubmissionCount(function(error, result) {console.log(JSON.stringify(result))});
//...

咱們使用Truffle給咱們的地址調用每一個合約,並分別爲每一個tokenstory建立一個實例。而後,咱們簡單地調用函數(與之前同樣異步)。控制檯給咱們兩個零,由於MetaMask中的賬戶有0個代幣,由於如今故事story中有0個提交。

3.讀取和輸出數據

最後,咱們可使用咱們提供的信息填充用戶的我的資料數據。

讓咱們更新咱們的JavaScript:

var loggedIn;

(function () {

    loggedIn = setLoggedIn(web3.currentProvider !== undefined && web3.eth.accounts.length > 0);

    if (loggedIn) {

        var token = TNSToken.at('0x3134bcded93e810e1025ee814e87eff252cff422');
        var story = StoryDao.at('0x729400828808bc907f68d9ffdeb317c23d2034d5');

        token.balanceOf(web3.eth.accounts[0], function(error, result) {console.log(JSON.stringify(result))});
        story.getSubmissionCount(function(error, result) {console.log(JSON.stringify(result))});

        readUserStats().then(User => renderUserInfo(User));
    }

})();

async function readUserStats(address) {
    if (address === undefined) {
        address = web3.eth.accounts[0];
    }
    var User = {
        numberOfSubmissions: await getSubmissionsCountForUser(address),
        numberOfDeletions: await getDeletionsCountForUser(address),
        isWhitelisted: await isWhitelisted(address),
        isBlacklisted: await isBlacklisted(address),
        numberOfProposals: await getProposalCountForUser(address),
        numberOfVotes: await getVotesCountForUser(address)
    }
    return User;
}

function renderUserInfo(User) {
    console.log(User);

    document.querySelector('#user_submissions').innerHTML = User.numberOfSubmissions;
    document.querySelector('#user_deletions').innerHTML = User.numberOfDeletions;
    document.querySelector('#user_proposals').innerHTML = User.numberOfProposals;
    document.querySelector('#user_votes').innerHTML = User.numberOfVotes;
    document.querySelector('dd.user_blacklisted').style.display = User.isBlacklisted ? 'inline-block' : 'none';
    document.querySelector('dt.user_blacklisted').style.display = User.isBlacklisted ? 'inline-block' : 'none';
    document.querySelector('dt.user_whitelisted').style.display = User.isWhitelisted ? 'inline-block' : 'none';
    document.querySelector('dd.user_whitelisted').style.display = User.isWhitelisted ? 'inline-block' : 'none';
}

async function getSubmissionsCountForUser(address) {
    if (address === undefined) {
        address = web3.eth.accounts[0];
    }
    return new Promise(function (resolve, reject) {
        resolve(0);
    });
}
async function getDeletionsCountForUser(address) {
    if (address === undefined) {
        address = web3.eth.accounts[0];
    }
    return new Promise(function (resolve, reject) {
        resolve(0);
    });
}
async function getProposalCountForUser(address) {
    if (address === undefined) {
        address = web3.eth.accounts[0];
    }
    return new Promise(function (resolve, reject) {
        resolve(0);
    });
}
async function getVotesCountForUser(address) {
    if (address === undefined) {
        address = web3.eth.accounts[0];
    }
    return new Promise(function (resolve, reject) {
        resolve(0);
    });
}
async function isWhitelisted(address) {
    if (address === undefined) {
        address = web3.eth.accounts[0];
    }
    return new Promise(function (resolve, reject) {
        resolve(false);
    });
}
async function isBlacklisted(address) {
    if (address === undefined) {
        address = web3.eth.accounts[0];
    }
    return new Promise(function (resolve, reject) {
        resolve(false);
    });
}

讓咱們更改我的資料信息部分:

<div class="logged in" style="display: none">
    <p>You are logged in!</p>
    <div class="avatar">

    </div>
    <dl>
        <dt>Submissions</dt>
        <dd id="user_submissions"></dd>
        <dt>Proposals</dt>
        <dd id="user_proposals"></dd>
        <dt>Votes</dt>
        <dd id="user_votes"></dd>
        <dt>Deletions</dt>
        <dd id="user_deletions"></dd>
        <dt class="user_whitelisted">Whitelisted</dt>
        <dd class="user_whitelisted">Yes</dd>
        <dt class="user_blacklisted">Blacklisted</dt>
        <dd class="user_blacklisted">Yes</dd>
    </dl>
</div>

你會注意到咱們在獲取數據時使用了promises,即便咱們的函數當前只是模擬函數:它們會當即返回平面數據。這是由於每一個函數都須要不一樣的時間來獲取咱們要求它獲取的數據,所以咱們將在填充User對象以前等待它們完成,而後將其傳遞給render函數,該函數更新了屏幕。

若是您對JS承諾不熟悉並但願瞭解更多信息,請參閱此帖子

如今,咱們全部的功能都是嘲笑; 咱們須要先作一些寫操做才能閱讀。 但首先咱們須要準備好注意那些寫做的發生!

監聽事件

爲了可以跟蹤合約發出的事件,咱們須要監聽它們——不然咱們將全部這些emit語句都放入代碼中。咱們構建的模擬UI的中間部分用於保存這些事件。

如下是咱們如何監聽區塊鏈發出的事件:

// Events

var WhitelistedEvent = story.Whitelisted(function(error, result) {
    if (!error) {
        console.log(result);
    }
})

這裏咱們在StoryDao合約的story實例上調用Whitelisted函數,並將回調傳遞給它。每當觸發此給定事件時,將自動調用此回調。所以,當用戶被列入白名單時,代碼將自動將該事件的輸出記錄到控制檯。

可是,這隻會獲取網絡挖掘的最後一個塊的最後一個事件。所以,若是從第1塊到第10塊觸發了幾個白名單事件,它只會向咱們展現第10塊中的那些事件,若是有的話。更好的方法是使用這種方法:

story.Whitelisted({}, { fromBlock: 0, toBlock: 'latest' }).get((error, eventResult) => {
  if (error) {
    console.log('Error in myEvent event handler: ' + error);
  } else {  
    // eventResult contains list of events!
    console.log('Event: ' + JSON.stringify(eventResult[0].args));
  }
});

注意:將上面的內容放在JS文件底部的一個單獨的部分,一個專門用於事件。

在這裏,咱們使用get函數,它容許咱們定義從中獲取事件的塊範圍。咱們使用0到最新,這意味着咱們能夠獲取此類型的全部事件。可是這增長了與上述監聽方法發生衝突的可能。監聽方法輸出最後一個塊的事件,get方法輸出全部這些事件。咱們須要一種方法來使JS忽略雙重事件。不要寫那些你已經從歷史中獲取的東西。咱們會進一步作到這一點,但就目前而言,讓咱們來處理白名單。

賬戶白名單

最後,讓咱們進行一些寫操做。

第一個也是最簡單的一個是白名單。請記住,要得到白名單,賬戶須要向DAO的地址發送至少0.01以太。你將在部署時得到此地址。若是你的Ganache/PoA鏈在本課程的各個部分之間從新啓動,那不要緊,只需使用truffle migrate --reset從新運行,你就能夠得到代幣和DAO的新地址。在個人例子中,DAO的地址是0x729400828808bc907f68d9ffdeb317c23d2034d5,個人代幣是0x3134bcded93e810e1025ee814e87eff252cff422

設置完全部內容後,讓咱們嘗試向DAO地址發送必定數量的以太。讓咱們嘗試0.05以太只是爲了好玩,因此咱們能夠看看DAO是否爲咱們提供額外的計算代幣,以支付超額費用。

注意:不要忘記自定義gas量——只需在21000限制之上再拍一個零——使用標記爲紅色的圖標。爲何這有必要?由於由簡單的ether發送(回調函數)觸發的函數執行超過21000的額外邏輯,這對於簡單發送就足夠了。因此咱們須要達到極限。不要擔憂:超出此限制的任何內容都會當即退款。有關gas如何工做的入門讀物,請參見[https://www.sitepoint.com/ethereum-transaction-costs]。

在交易確認後(你將在MetaMask中將其視爲「已確認」),咱們能夠在MetaMask賬戶中檢查代幣金額。咱們首先須要將自定義代幣添加到MetaMask中,以便跟蹤它們。根據下面的動畫,過程以下:選擇MetaMask菜單,向下滾動到Add Tokens,選擇Custom Token,粘貼Truffle在遷移時給你的代幣地址,點擊Next,查看餘額是否爲ok,而後選擇添加Add Tokens

對於0.05 eth,咱們應該有400k令牌,咱們這樣作。

可是這個事件怎麼樣?咱們收到了這個白名單的通知嗎?咱們來看看控制檯吧。

實際上,完整的數據集就在那​​裏——發出事件的地址,塊數和挖掘它的hash,等等。其中包括args對象,它告訴咱們事件數據:addr是被列入白名單的地址,狀態是它是添加到白名單仍是從中刪除。成功!

若是咱們如今刷新頁面,則事件再次出如今控制檯中。可是怎麼樣?咱們沒有將任何新人列入白名單。爲何事件會發生警告?EVM中的事件是它們不像JavaScript那樣是一次性的事情。固然,它們包含任意數據並僅用做輸出,但它們的輸出永遠在區塊鏈中註冊,由於致使它們的交易也永久地在區塊鏈中註冊。所以事件將在發出以後保留,這使咱們沒必要將它們存儲在某處並在頁面刷新時調用它們!

如今讓咱們將其添加到UI中的事件屏幕!編輯JavaScript文件的Events部分,以下所示:

// Events

var highestBlock = 0;
var WhitelistedEvent = story.Whitelisted({}, { fromBlock: 0, toBlock: "latest" });

WhitelistedEvent.get((error, eventResult) => {
  if (error) {
    console.log('Error in Whitelisted event handler: ' + error);
  } else {  
    console.log(eventResult);
    let len = eventResult.length;
    for (let i = 0; i < len; i++) {
      console.log(eventResult[i]);
      highestBlock = highestBlock < eventResult[i].blockNumber ? eventResult[i].blockNumber : highestBlock;
      printEvent("Whitelisted", eventResult[i]);
    }
  }
});

WhitelistedEvent.watch(function(error, result) {
  if (!error && result.blockNumber > highestBlock) {
    printEvent("Whitelisted", result);
  }
});

function printEvent(type, object) {
  switch (type) {
    case "Whitelisted":
      let el;
      if (object.args.status === true) {
          el = "<li>Whitelisted address "+ object.args.addr +"</li>";
      } else {
          el = "<li>Removed address "+ object.args.addr +" from whitelist!</li>";
      }
      document.querySelector("ul.eventlist").innerHTML += el;
    break;
    default:
    break;
  }
}

哇,變得很快,是吧?不用擔憂,咱們會澄清。

highestBlock變量將記住從歷史記錄中獲取的最新塊。咱們建立了一個事件的實例,併爲它附加了兩個監聽器。一個是get,它從歷史記錄中獲取全部事件並記住最新的塊。另外一個是watchwatch事件「實時」並在最近一個塊中出現新事件時觸發。只有當剛剛進入的塊大於咱們記憶中最高的塊時,觀察者纔會觸發,確保只有新事件被附加到事件列表中。

咱們還添加了一個printEvent函數執行打印事件的操做; 咱們也能夠將它重複用於其餘類型的事件!

若是咱們如今測試它,確實,咱們能夠很好地打印出來。

如今嘗試本身作這個,咱們的故事Story能夠發出的全部其餘事件!看看你是否能夠弄清楚如何一次處理它們,而沒必要爲每一個都寫出這個邏輯。(提示:在數組中定義它們的名稱,而後遍歷這些名稱並動態註冊事件!)

手動檢查

你還能夠經過在MyEtherWallet中打開並調用其whitelist函數來手動檢查StoryBAO的白名單和全部其餘公共參數。

你會注意到,若是咱們檢查剛剛發送白名單金額的賬戶,咱們將得到true回覆,代表此賬戶確實存在於whitelist映射中。

在將其添加到Web UI以前,使用此相同的功能菜單來試驗其餘功能。

提交參賽做品

最後,讓咱們從UI進行正確的寫函數調用。這一次,咱們將在故事Story中提交一個條目。首先,咱們須要清除咱們在開始時放在那裏的示例條目。編輯HTML到這個:

<div class="content container">
    <div class="intro">
        <h3>Chapter 0</h3>
        <p class="intro">It's a rainy night in central London.</p>
    </div>
    <hr>
    <div class="submission_input">
        <textarea name="submission-body" id="submission-body-input" rows="5"></textarea>
        <button id="submission-body-btn">Submit</button>
    </div>
    ...

還有一些基本的CSS:

.submission_input textarea {
  width: 100%;
}

咱們添加了一個很是簡單的textarea,用戶能夠經過它提交新條目。

咱們如今來作JS部分吧。

首先,讓咱們準備經過添加一個新事件並修改咱們的printEvent函數來接受這個事件。咱們還能夠對整個事件部分進行一些重構,以使其更具可重用性。

// Events

var highestBlock = 0;
var WhitelistedEvent = story.Whitelisted({}, { fromBlock: 0, toBlock: "latest" });
var SubmissionCreatedEvent = story.SubmissionCreated({}, { fromBlock: 0, toBlock: "latest" });

var events = [WhitelistedEvent, SubmissionCreatedEvent];
for (let i = 0; i < events.length; i++) {
  events[i].get(historyCallback);
  events[i].watch(watchCallback);
}

function watchCallback(error, result) {
  if (!error && result.blockNumber > highestBlock) {
    printEvent(result.event, result);
  }
}

function historyCallback(error, eventResult) {
  if (error) {
    console.log('Error in event handler: ' + error);
  } else {  
    console.log(eventResult);
    let len = eventResult.length;
    for (let i = 0; i < len; i++) {
      console.log(eventResult[i]);
      highestBlock = highestBlock < eventResult[i].blockNumber ? eventResult[i].blockNumber : highestBlock;
      printEvent(eventResult[i].event, eventResult[i]);
    }
  }
}

function printEvent(type, object) {
  let el;
  switch (type) {
    case "Whitelisted":
      if (object.args.status === true) {
          el = "<li>Whitelisted address "+ object.args.addr +"</li>";
      } else {
          el = "<li>Removed address "+ object.args.addr +" from whitelist!</li>";
      }
      document.querySelector("ul.eventlist").innerHTML += el;
    break;
    case "SubmissionCreated":
      el = "<li>User " + object.args.submitter + " created a"+ ((object.args.image) ? "n image" : " text") +" entry: #" + object.args.index + " of content " + object.args.content+"</li>";
      document.querySelector("ul.eventlist").innerHTML += el;
    break;
    default:
    break;
  }
}

如今咱們須要作的就是添加一個全新的事件來實例化它,而後爲它定義一個case。

接下來,讓咱們提交。

document.getElementById("submission-body-btn").addEventListener("click", function(e) {
    if (!loggedIn) {
        return false;
    }

    var text = document.getElementById("submission-body-input").value;
    text = web3.toHex(text);

    story.createSubmission(text, false, {value: 0, gas: 400000}, function(error, result) {
        refreshSubmissions();
    });
});

function refreshSubmissions() {
    story.getAllSubmissionHashes(function(error, result){
        console.log(result);
    });
}

在這裏,咱們向提交表單添加一個事件監聽器,一旦提交,首先拒絕全部用戶未登陸的內容,而後抓取內容並將其轉換爲十六進制格式(這是咱們須要將值存儲爲bytes )。

最後,它經過調用createSubmission函數並提供兩個參數來建立交易:條目的文本和false標記(意思即不是圖像)。第三個參數是交易設置:值表示要發送多少以太,而gas表示你想要默認的gas限制量。這能夠在客戶端(MetaMask)中手動更改,但這是一個很好的起點,以確保咱們不會遇到限制。最後一個參數是咱們如今已經習慣的回調函數,這個回調函數將調用一個刷新函數來加載故事Story的全部提交。目前,此刷新功能僅加載故事story哈希並將它們放入控制檯,以便咱們檢查一切是否正常。

注意:以太量爲0,由於第一個條目是免費的。進一步的條目將須要添加以太幣。咱們將動態計算留給你看成業。提示:爲此目的,咱們的DAO中有一個calculateSubmissionFee函數。

此時,咱們須要在JS的頂部更改一些在頁面加載時自動執行的函數:

if (loggedIn) {

    token.balanceOf(web3.eth.accounts[0], function(error, result) {console.log(JSON.stringify(result))});
    story.getSubmissionCount(function(error, result) {console.log(JSON.stringify(result))});

    web3.eth.defaultAccount = web3.eth.accounts[0]; // CHANGE

    readUserStats().then(User => renderUserInfo(User));
    refreshSubmissions(); // CHANGE
} else {
    document.getElementById("submission-body-btn").disabled = "disabled";
}

更改標記爲//CHANGE:第一個容許咱們設置執行交易的默認賬戶。這可能會在將來的Web3版本中默認使用。第二個刷新頁面加載時提交的內容,所以咱們在網站打開時得到一個完整的故事story。

若是你如今嘗試提交條目,MetaMask應在你單擊「提交」後當即打開,並要求你確認提交。

你還應該在事件部分中看到事件打印出來。

控制檯應該回顯這個新條目的哈希值。

注意:MetaMask目前在私有網絡和nonce方面存在問題。它在這裏描述並將很快修復,但若是nonce在提交條目時在JavaScript控制檯中收到錯誤,那麼目前的權宜之計解決方案是從新安裝MetaMask(禁用和啓用將不起做用)。請記住首先備份你的種子SEED:你須要它來從新導入你的MetaMask賬戶!

最後,讓咱們獲取這些條目並顯示它們。讓咱們從一些CSS開始:

.content-submissions .submission-submitter { font-size: small; }

如今讓咱們更新一下這個refreshSubmissions功能:

 

咱們瀏覽全部提交內容,獲取它們的哈希值,獲取每一個哈希值,而後在屏幕上輸出。若是提交者與登陸用戶相同,則打印「你」而不是地址。

讓咱們添加另外一個條目進行測試。

結論

在這一部分中,咱們爲DApp開發了基本前端的開端。

因爲開發完整的前端應用程序也能夠成爲它本身的一個過程,咱們將做爲家庭做業留給你進一步的發展。只需調用所演示的函數,將它們綁定到常規JavaScript流程中(經過像VueJS這樣的框架或普通的舊jQuery或像咱們上面所作的原生JS)並將它們綁定在一塊兒。它實際上就像與標準服務器API交談。若是你遇到困難,請查看代碼的項目倉庫!

能夠執行的其餘升級:

  • 檢測web3提供程序什麼時候更改或可用賬戶數什麼時候更改,指示登陸或註銷事件並自動從新加載頁面。
  • 除非用戶已登陸,不然將阻止呈現提交表單。
  • 防止呈現投票和刪除按鈕,除非用戶至少有1個代幣等。
  • 讓人們提交併呈現Markdown!
  • 按時間(塊號)訂購事件,而不是按類型訂購!
  • 使事件更漂亮,更可讀:不是顯示十六進制內容,而是將其翻譯爲ASCII並截斷爲30個左右的字符。
  • 使用像VueJS這樣的適當的JS框架來從項目中得到一些可重用性並得到更好的結構化代碼。

在下一部分和最後一部分中,咱們將專一於將咱們的項目部署到實時互聯網。敬請關注!

======================================================================

分享一些以太坊、EOS、比特幣等區塊鏈相關的交互式在線編程實戰教程:

  • java以太坊開發教程,主要是針對java和android程序員進行區塊鏈以太坊開發的web3j詳解。
  • python以太坊,主要是針對python工程師使用web3.py進行區塊鏈以太坊開發的詳解。
  • php以太坊,主要是介紹使用php進行智能合約開發交互,進行帳號建立、交易、轉帳、代幣開發以及過濾器和交易等內容。
  • 以太坊入門教程,主要介紹智能合約與dapp應用開發,適合入門。
  • 以太坊開發進階教程,主要是介紹使用node.js、mongodb、區塊鏈、ipfs實現去中心化電商DApp實戰,適合進階。
  • C#以太坊,主要講解如何使用C#開發基於.Net的以太坊應用,包括帳戶管理、狀態與交易、智能合約開發與交互、過濾器和交易等。
  • EOS教程,本課程幫助你快速入門EOS區塊鏈去中心化應用的開發,內容涵蓋EOS工具鏈、帳戶與錢包、發行代幣、智能合約開發與部署、使用代碼與智能合約交互等核心知識點,最後綜合運用各知識點完成一個便籤DApp的開發。
  • java比特幣開發教程,本課程面向初學者,內容即涵蓋比特幣的核心概念,例如區塊鏈存儲、去中心化共識機制、密鑰與腳本、交易與UTXO等,同時也詳細講解如何在Java代碼中集成比特幣支持功能,例如建立地址、管理錢包、構造裸交易等,是Java工程師不可多得的比特幣開發學習課程。
  • php比特幣開發教程,本課程面向初學者,內容即涵蓋比特幣的核心概念,例如區塊鏈存儲、去中心化共識機制、密鑰與腳本、交易與UTXO等,同時也詳細講解如何在Php代碼中集成比特幣支持功能,例如建立地址、管理錢包、構造裸交易等,是Php工程師不可多得的比特幣開發學習課程。
  • tendermint區塊鏈開發詳解,本課程適合但願使用tendermint進行區塊鏈開發的工程師,課程內容即包括tendermint應用開發模型中的核心概念,例如ABCI接口、默克爾樹、多版本狀態庫等,也包括代幣發行等豐富的實操代碼,是go語言工程師快速入門區塊鏈開發的最佳選擇。

匯智網原創翻譯,轉載請標明出處。這裏是原文以太坊構建DApps系列教程(七):爲DAO合約構建Web3 UI

相關文章
相關標籤/搜索