以太坊構建DApps系列教程(五):智能合約通訊和代幣銷售

在本系列關於使用以太坊構建DApps教程的第4部分中,咱們開始構建和測試咱們的DAO智能合約。 如今讓咱們更進一步,根據咱們的介紹,處理向故事Story添加內容和代幣。php

添加代幣

對於可以與另外一個合約進行交互的合約,它須要知道其餘合約的接口——可用的函數。因爲咱們的TNS代幣具備至關簡單的接口,所以咱們能夠將其包含在DAO的智能合約中, contract StoryDao聲明之上以及咱們的import語句中加入:html

contract LockableToken is Ownable {
    function totalSupply() public view returns (uint256);
    function balanceOf(address who) public view returns (uint256);
    function transfer(address to, uint256 value) public returns (bool);
    event Transfer(address indexed from, address indexed to, uint256 value);
    function allowance(address owner, address spender) public view returns (uint256);
    function transferFrom(address from, address to, uint256 value) public returns (bool);
    function approve(address spender, uint256 value) public returns (bool);
    event Approval(address indexed owner, address indexed spender, uint256 value);
    function approveAndCall(address _spender, uint256 _value, bytes _data) public payable returns (bool);
    function transferAndCall(address _to, uint256 _value, bytes _data) public payable returns (bool);
    function transferFromAndCall(address _from, address _to, uint256 _value, bytes _data) public payable returns (bool);

    function increaseLockedAmount(address _owner, uint256 _amount) public returns (uint256);
    function decreaseLockedAmount(address _owner, uint256 _amount) public returns (uint256);
    function getLockedAmount(address _owner) view public returns (uint256);
    function getUnlockedAmount(address _owner) view public returns (uint256);
}

請注意,咱們不須要粘貼函數的「內容」,而只須要粘貼它們的簽名(骨架)。這就是合約之間交互所需的所有內容。java

如今咱們能夠在DAO合約中使用這些函數。計劃以下:node

  • 啓動代幣(咱們已經這樣作了)。
  • 從同一地址啓動DAO。
  • 將全部代幣從代幣啓動器發送到DAO,而後經過合約將全部權轉移到DAO自己。
  • 此時,DAO擁有全部代幣並可使用發送功能將其出售給人員,或者可使用批准功能(在投票期間有用)等將其保留用於支出。

但DAO如何知道部署代幣的地址?咱們告訴它。python

首先,咱們在DAO合約的頂部添加一個新變量:android

LockableToken public token;

而後,咱們添加一些函數:程序員

constructor(address _token) public {
    require(_token != address(0), "Token address cannot be null-address");
    token = LockableToken(_token);
}

構造函數是在部署合約時自動調用的函數。它對於初始化連接合約,默認值等值頗有用。在咱們的例子中,咱們將使用它來使用和保存TNS代幣的地址。require檢查是爲了確保代幣的地址有效。web

在咱們處理它時,讓咱們添加一個函數,讓用戶能夠檢查DAO中待售的代幣數量,以及更改成另外一個代幣的函數,若是出現問題而且須要進行此類更改。這種變化也須要一個事件,因此咱們也要添加它。算法

event TokenAddressChange(address token);

function daoTokenBalance() public view returns (uint256) {
    return token.balanceOf(address(this));
}

function changeTokenAddress(address _token) onlyOwner public {
    require(_token != address(0), "Token address cannot be null-address");
    token = LockableToken(_token);
    emit TokenAddressChange(_token);
}

第一個函數設置爲view由於它不會改變區塊鏈的狀態;它不會改變任何值。這意味着它是對區塊鏈的免費,只讀函數調用:它不須要付費交易。它還將標記的餘額做爲數字返回,所以須要在函數的簽名上使用returns (uint256)進行聲明。代幣有一個balanceOf函數(參見咱們上面粘貼的接口),它接受一個參數——要檢查其他額的地址。咱們正在檢查DAO的餘額,咱們將「this」變成一個address()mongodb

代幣地址更改功能容許全部者(admin)更改代幣地址。它與構造函數的邏輯相同。

讓咱們看看咱們如何讓人們如今購買代幣。

購買代幣

根據該系列的前一部分,用戶能夠經過如下方式購買代幣:

  • 若是已經列入白名單,請使用後備功能。換句話說,只需將以太送到DAO合約便可。
  • 使用whitelistAddress功能發送超過白名單所需的費用。
  • 直接調用buyTokens函數。

可是,有一個警告。當有人從外部調用buyTokens函數時,若是DAO中沒有足夠的代幣可供出售,咱們但願它失敗提示。可是當有人經過白名單功能經過在第一次白名單嘗試中發送太多以太來購買代幣時,咱們不但願它失敗,由於白名單處理過程將被取消。以太坊中的交易要麼一切都必須成功,要麼就是一無所得。因此咱們將製做兩個buyTokens函數。

// This goes at the top of the contract with other properties
uint256 public tokenToWeiRatio = 10000;

function buyTokensThrow(address _buyer, uint256 _wei) external {

    require(whitelist[_buyer], "Candidate must be whitelisted.");
    require(!blacklist[_buyer], "Candidate must not be blacklisted.");

    uint256 tokens = _wei * tokenToWeiRatio;
    require(daoTokenBalance() >= tokens, "DAO must have enough tokens for sale");
    token.transfer(_buyer, tokens);
}

function buyTokensInternal(address _buyer, uint256 _wei) internal {
    require(!blacklist[_buyer], "Candidate must not be blacklisted.");
    uint256 tokens = _wei * tokenToWeiRatio;
    if (daoTokenBalance() < tokens) {
        msg.sender.transfer(_wei);
    } else {
        token.transfer(_buyer, tokens);
    }
}

所以,存在1億個TNS代幣。若是咱們爲每一個以太設置10000個代幣的價格,則每一個代幣的價格降至4-5美分,這是能夠接受的。

這些函數在對違禁用戶和其餘因素進行完整性檢查後進行一些計算,並當即將代幣發送給買方,買方能夠按照本身的意願開始使用它們——不管是投票仍是在交易所銷售。若是DAO中的代幣數量少於買方試圖購買的代幣,則退還買方。

部分token.transfer(_buyer, tokens)是咱們使用TNS代幣合約來啓動從當前位置(DAO)到目標_buyertokens金額。

如今咱們知道人們能夠得到代幣,讓咱們看看咱們是否能夠實施提交。

結構和提交

根據咱們的介紹帖子,提交一個條目將花費0.0001 eth倍於故事中的條目數量。咱們只須要計算未刪除的提交(由於提交能夠刪除),因此讓咱們添加這個所需的屬性和一個方法來幫助咱們。

uint256 public submissionZeroFee = 0.0001 ether;
uint256 public nonDeletedSubmissions = 0;

function calculateSubmissionFee() view internal returns (uint256) {
    return submissionZeroFee * nonDeletedSubmissions;
}

注意:Solidity具備內置時間和以太單位。在這裏閱讀更多相關信息。

此費用只能由業主更改,但只能下降。爲了增長,須要投票。讓咱們寫下減函數:

function lowerSubmissionFee(uint256 _fee) onlyOwner external {
    require(_fee < submissionZeroFee, "New fee must be lower than old fee.");
    submissionZeroFee = _fee;
    emit SubmissionFeeChanged(_fee);
}

咱們發出一個事件來通知全部觀察客戶費用已經改變,因此讓咱們聲明這個事件:

event SubmissionFeeChanged(uint256 newFee);

提交能夠是最多256個字符的文本,而且相同的限制適用於圖像。只有他們的類型改變。這是自定義結構的一個很好的用例。讓咱們定義一個新的數據類型。

struct Submission {
    bytes content;
    bool image;
    uint256 index;
    address submitter;
    bool exists;
}

這就像咱們智能合約中的「對象類型」。該對象具備不一樣類型的屬性。contentbytes類型值。image屬性是一個布爾值,表示它是不是圖像(true/false)。index是一個數字等於提交時的順序數字; 它在全部提交列表中的索引(0,1,2,3 ......)。submitter是提交條目的賬戶的地址,而且exists標誌,由於在映射中,即便密鑰尚不存在,全部密鑰的全部值都被初始化爲默認值(false)。

換句話說,當你有一個address => bool映射時,該映射已經將世界上的全部地址都設置爲「false」。這就是以太坊的運做方式。所以,經過檢查提交是否存在於某個哈希,咱們會獲得「是」,而提交可能根本就不存在。存在標誌有助於此。它讓咱們檢查提交是否存在且存在——即提交,而不是僅由EVM隱式添加。此外,它使之後更容易「刪除」條目。

注意:從技術上講,咱們還能夠檢查以確保提交者的地址不是零地址。

當咱們在這裏時,讓咱們定義兩個事件:一個用於刪除條目,一個用於建立條目。

event SubmissionCreated(uint256 index, bytes content, bool image, address submitter);
event SubmissionDeleted(uint256 index, bytes content, bool image, address submitter);

可是有一個問題。以太坊中的映射是不可迭代的:咱們沒法在沒有嚴重黑客攻擊的狀況下遍歷它們。

爲了遍歷它們,咱們將爲這些提交建立一個標識符數組,其中數組的鍵將是提交的索引,而值將是咱們將爲每一個提交生成的惟一哈希值。keccak256爲咱們提供了keccak256哈希算法,用於從任意值生成哈希值,咱們能夠將其與當前塊號一塊兒使用,以確保條目不會在同一塊中重複,併爲每一個條目得到必定程度的惟一性。咱們這樣使用它: keccak256(abi.encodePacked(_content, block.number));。咱們須要encodePacked傳遞給算法的變量,由於它須要咱們的一個參數。這就是這個函數的做用。

咱們還須要在某處存儲提交內容,因此讓咱們再定義兩個合約變量。

mapping (bytes32 => Submission) public submissions;
bytes32[] public submissionIndex;

好的,咱們如今嘗試構建createSubmission函數。

function createSubmission(bytes _content, bool _image) external payable {
 uint256 fee = calculateSubmissionFee();
 require(msg.value >= fee, "Fee for submitting an entry must be sufficient.");
 bytes32 hash = keccak256(abi.encodePacked(_content, block.number));
 require(!submissions[hash].exists, "Submission must not already exist in same block!");
 submissions[hash] = Submission( _content, _image, submissionIndex.push(hash), msg.sender, true );
 emit SubmissionCreated( submissions[hash].index, submissions[hash].content, submissions[hash].image, submissions[hash].submitter ); nonDeletedSubmissions += 1; 
}

讓咱們逐行說明:

function createSubmission(bytes _content, bool _image) external payable {

該函數接受字節內容(字節是一個動態大小的字節數組,對存儲任意數量的數據頗有用)和一個布爾標誌,表示該輸入是不是圖像。該函數只能從外部世界調用,而且應支付,這意味着它在交易調用時接受以太。

uint256 fee = calculateSubmissionFee();
require(msg.value >= fee, "Fee for submitting an entry must be sufficient.");

接下來,咱們計算提交新條目的成本,而後檢查與交易一塊兒發送的價值是否等於或大於費用。

bytes32 hash = keccak256(abi.encodePacked(_content, block.number));
require(!submissions[hash].exists, "Submission must not already exist in same block!");

而後咱們計算這個條目的哈希值(bytes32是一個32字節的固定大小數組,因此32個字符也是keccak256輸出)。咱們使用此哈希來查明是否已存在具備該哈希的提交,若是確實存在,則取消全部內容。

submissions[hash] = Submission( _content, _image, submissionIndex.push(hash), msg.sender, true );

此部分在submissions映射中的哈希位置建立新提交。它只是經過合約中上面定義的新結構傳遞值。請注意,雖然你可能習慣使用其餘語言的new關鍵字,但這裏沒有必要(或容許)。而後咱們發出事件(不言自明),最後,還有nonDeletedSubmissions += 1;:這是增長下次提交費用的緣由(參見calculateSubmissionFee)。

可是這裏缺乏不少邏輯。咱們仍然須要:

  • 圖像的賬戶
  • 檢查提交賬戶的白名單/黑名單存在和1個TNS代幣全部權。

咱們先作圖像吧。咱們的原始計劃表示,每50個文本只能提交一張圖像。咱們還須要兩個合約屬性:

uint256 public imageGapMin = 50;
uint256 public imageGap = 0;

固然你已經能夠假設咱們將如何處理這個問題?讓咱們在建立新submissions[hash] = ...的以前當即將如下內容添加到咱們的createSubmission方法中。

if (_image) {
    require(imageGap >= imageGapMin, "Image can only be submitted if more than {imageGapMin} texts precede it.");
    imageGap = 0;
} else {
    imageGap += 1;
}

很是簡單:若是條目應該是圖像,那麼首先檢查圖像之間的間隙是否超過49,若是是,則將其重置爲0。不然,將間隙增長一。就像那樣,每50次(或更屢次)提交現有內容能夠成爲一個圖像。

最後,讓咱們進行訪問檢查。咱們能夠在費用計算以前和緊接在函數入口點以後放置此代碼,由於訪問檢查應該首先發生。

require(token.balanceOf(msg.sender) >= 10**token.decimals());
require(whitelist[msg.sender], "Must be whitelisted");
require(!blacklist[msg.sender], "Must not be blacklisted");

第一行檢查消息發送者是否具備比代幣合約中小數位數更多的代幣(由於咱們能夠更改代幣地址,所以可能另外一個代幣將在稍後使用咱們的代幣,而且可能沒有18位小數。)。換句話說,在咱們的例子中,10**token.decimals10**18,即1000 000 000 000 000 000,1後跟18個零。若是咱們的代幣有18位小數,那就是1.000000000000000000,或者是一(1)個TNS代幣。請注意,在分析此代碼時,你的編譯器或linter可能會給你一些警告。這是由於代幣的decimals屬性是公共的,所以它的getter函數是decimals()自動生成的,但它沒有明確列在咱們在合約頂部列出的代幣的接口中。爲了解決這個問題,咱們能夠經過添加如下行來更改接口:

function decimals() public view returns (uint256);

還有一件事:由於使用目前設定爲1%的合約的全部者費用,讓咱們放棄全部者能夠提取的金額並將其他部分保留在DAO中。最簡單的方法是跟蹤全部者能夠提取多少,並在每次提交建立後增長該數量。讓咱們在合約中添加一個新屬性:

uint256 public withdrawableByOwner = 0;

而後將其添加到咱們的createSubmission函數的末尾:

withdrawableByOwner += fee.div(daofee);

咱們能夠經過這樣的功能讓全部者退出:

function withdrawToOwner() public {
 owner.transfer(withdrawableByOwner);
 withdrawableByOwner = 0;
}

這會將容許的金額發送給全部者,並將計數器重置爲0.若是全部者不想取出所有金額,咱們能夠爲該狀況添加另外一個函數:

function withdrawAmountToOwner(uint256 _amount) public {
    uint256 withdraw = _amount;
    if (withdraw > withdrawableByOwner) {
        withdraw = withdrawableByOwner;
    }
    owner.transfer(withdraw);
    withdrawableByOwner = withdrawableByOwner.sub(withdraw);
}

因爲咱們常常會經過哈希引用提交,讓咱們編寫一個函數來檢查提交是否存在,以便咱們能夠替換咱們的submissions[hash].exists檢查:

function submissionExists(bytes32 hash) public view returns (bool) { return submissions[hash].exists; }

還須要一些其餘幫助函數來讀取提交內容:

function getSubmission(bytes32 hash) public view returns (bytes content, bool image, address submitter) {
    return (submissions[hash].content, submissions[hash].image, submissions[hash].submitter);
}

function getAllSubmissionHashes() public view returns (bytes32[]) {
    return submissionIndex;
}

function getSubmissionCount() public view returns (uint256) {
    return submissionIndex.length;
}

getSubmission獲取提交數據,getAllSubmissionHashes獲取系統中的全部惟一哈希,getSubmissionCount列出總共提交的數量(包括已刪除的提交)。咱們在客戶端(在UI中)使用這些功能的組合來獲取內容。

完整的createSubmission函數如今看起來像這樣:

function createSubmission(bytes _content, bool _image) storyActive external payable {

    require(token.balanceOf(msg.sender) >= 10**token.decimals());
    require(whitelist[msg.sender], "Must be whitelisted");
    require(!blacklist[msg.sender], "Must not be blacklisted");

    uint256 fee = calculateSubmissionFee();
    require(msg.value >= fee, "Fee for submitting an entry must be sufficient.");

    bytes32 hash = keccak256(abi.encodePacked(_content, block.number));
    require(!submissionExists(hash), "Submission must not already exist in same block!");

    if (_image) {
        require(imageGap >= imageGapMin, "Image can only be submitted if more than {imageGapMin} texts precede it.");
        imageGap = 0;
    } else {
        imageGap += 1;
    }

    submissions[hash] = Submission(
        _content,
        _image,
        submissionIndex.push(hash),
        msg.sender,
        true
    );

    emit SubmissionCreated(
        submissions[hash].index,
        submissions[hash].content,
        submissions[hash].image,
        submissions[hash].submitter
    );

    nonDeletedSubmissions += 1;
    withdrawableByOwner += fee.div(daofee);
}

刪除

那麼刪除提交呢?這很容易:咱們只是將exists標誌切換爲false

function deleteSubmission(bytes32 hash) internal {
    require(submissionExists(hash), "Submission must exist to be deletable.");
    Submission storage sub = submissions[hash];

    sub.exists = false;
    deletions[submissions[hash].submitter] += 1;

    emit SubmissionDeleted(
        sub.index,
        sub.content,
        sub.image,
        sub.submitter
    );

    nonDeletedSubmissions -= 1;
}

首先,咱們確保提交存在且還沒有刪除;而後咱們從存儲中檢索它。接下來,咱們將其exists標誌設置爲false,將該地址的DAO中的刪除次數增長1(在跟蹤用戶之後刪除的條目數時很是有用;這可能致使黑名單!),咱們發出刪除事件。

最後,咱們經過減小系統中未刪除的提交數量來減小新的提交建立費用。咱們不要忘記在咱們的合約中添加一個新屬性:一個用於跟蹤這些刪除。

mapping (address => uint256) public deletions;

部署變得更加複雜

如今咱們在另外一個合約中使用代幣,咱們須要更新部署腳本(3_deploy_storydao)以將代幣的地址傳遞給StoryDao的構造函數,以下所示:

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

module.exports = function(deployer, network, accounts) {
  if (network == "development") {
    deployer.deploy(StoryDao, TNSToken.address, {from: accounts[0]});
  } else {
    deployer.deploy(StoryDao, TNSToken.address);
  }
};

閱讀有關配置部署的更多信息。

結論

在這一部分中,咱們添加了參與者從咱們的DAO購買代幣並在故事Story中添加提交的能力。DAO合約的另外一部分功能仍然是:投票和民主化。這就是咱們將在下一篇文章中處理的內容。

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

分享一些以太坊、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系列教程(五):智能合約通訊和代幣銷售

相關文章
相關標籤/搜索