65.9K
CodeProject 正在变化。 阅读更多。
Home

Etherdrop:以太坊区块链上的 DAPP 彩票

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.82/5 (3投票s)

2018 年 8 月 10 日

CPOL

9分钟阅读

viewsIcon

16260

downloadIcon

242

您的以太坊赠品智能合约彩票。

引言

Etherdrop.app

当然,我们都听说过“比特币”和中本聪带来的著名区块链技术。

比特币是真正的货币互联网的第一种形式,而以太坊是“去中心化计算无信任平台互联网”的第一种形式!

区块链技术使我们能够通过信任网络进行通信,其中共识由密码学、算法和节点/矿工的力量强制执行。

我不会过多解释比特币、以太坊或其他形式的加密货币。[感谢 Google]。

以太坊的创造者天才说得很好

以太坊是一个去中心化的计算平台,允许用户与他人互动,
使用一种数字合约,即“智能合约”,它在各方之间强制执行特定行为
一旦部署在区块链上,就可以明确定义去中心化合约和不可变行为!

在以太坊上,我们有两种类型的账户(地址)

  1. 用户和各方将拥有其“用户”账户(钱包地址),其中持有 ETH 以太币余额。
  2. 智能合约(“不可变的透明行为程序”)地址,其中包含 ETH 和额外的二进制(EVM 指令 - 合约操作码)结构和数据(合约状态变量)。

背景

让我们以彩票娱乐服务的“智能合约”为例

  1. 许多人将支付门票以订阅
  2. 等待本轮结束
  3. 将从奖池中随机选出一位获胜者!

您可以在 etherdrop.app 上实时查看 Etherdrop

旧的经典方式是您永远无法访问服务背后的中心化机构(服务器)来了解或验证如何选择此随机用户,以及幕后发生的事情,即使有人为监控/监管的存在。

在本文中,您将学习如何:

  1. 使用 Solidity 语言开发彩票服务逻辑 [智能合约]
  2. 使用 NodeJs 和 Truffle 将智能合约部署到本地区块链 (Ganache) / 或以太坊主网区块链
  3. 开发 Web
    前端 - UI - Materialized, JS, Jquery, Web3Js, TruffleJs
    支持 Web3(下一代 Web)以与 以太坊区块链 交互
  4. 开发 Web 后端 - NodeJS
  5. Etherscan 上分析和读取区块链数据

项目结构

Using the Code

我们将逐节进行 [ A . B . C . D . E ]

[ A ]

  • 开始我们的智能合约开发,智能合约是一种 类(在 Java、C#、OOP 中),包含状态变量(例如,全局变量)以及函数。
  • 请注意,智能合约函数的任何执行都需要从用户账户地址到智能合约地址的链上交易,指向特定函数(及其数据参数)
  • 要执行和确认的交易需要添加到区块中,该区块应添加到区块链中(已确认),例如,3 次确认意味着交易区块之后的 3 个区块已添加到区块链中,依此类推(一个区块包含许多交易,取决于区块大小……)
  • 而读取或调用不改变状态变量或合约数据的被动函数(view)是免费且几乎即时的。
  • 将选择您的交易的矿工(或节点)将广播给其他节点,并选择它进行执行并添加到下一个区块(因此在区块链中)。
  • 执行将在矿工节点硬件(GPU、CPU...)上进行,其中合约及其变量的数据在区块链上,这是一种分布式去中心化(原始文件或数据库),包括整个用户网络、合约数据
  • 执行不是免费的,因为矿工正在运行其硬件并消耗电力、能源、互联网等...
  • 以太坊中的执行价格是“GAS”消耗,其中 EVM 上的每个 OPCODE 将花费特定数量的 GAS
  • GAS 价格以 WEI 为单位(1 以太币 = 10^18 Wei
  • 交易将始终包括
    • GAS 限制(即您愿意花费的最大 GAS 量)
    • GAS 价格(即您希望为每个花费的 GAS 支付给矿工的费用)
  • 请注意,GAS 限制将确保任何交易(代码执行)都不会挂起或花费超过其价格的时间,并且不会存在糟糕的无限循环。
  • 如果矿工的 GAS 消耗达到交易账户设置的限制,交易仍将被挖矿,但不会被执行,以太币金额将被撤销,但已花费的 GAS 不会,并且合约状态将回滚!交易状态为失败 - GAS 不足!

    * 通过安装 GIT 分布式版本控制系统来准备您的环境
    * 安装 nodejsnode 包管理器

    记事本++ 或任何文本编辑器基本上就足够了...

    打开命令行终端 [确保 git、npm 已安装并设置为系统路径(环境变量)]

    > git clone git@bitbucket.org:braingerm/cp-etherdrop.git

    > cd <project_root_folder_path>

    > npm install (这将从 packages.json 安装依赖项和 truffle 到 node_modules 中)

    * 安装本地区块链仿真节点 Ganache

    * 运行 Ganache

  • 它将默认监听 localhost 端口 7545
  • 它将默认创建 10 个账户,每个账户带有助记词(每个 100 ETH)
  • 助记词可以导入到 Metamask 或其他支持的钱包中。
    它更像是您的账户密码(私钥)

    现在大多数工具都已准备就绪,是时候解释、编译和部署 EtherDrop 智能合约了,
    鉴于项目结构,EtherDrop 智能合约位于 ./contracts/EtherDrop.sol 下。

    *.sol 扩展名代表 solidity。

    pragma solidity ^0.4.20; // This is the Solidity Used Compiler Version
    
    /**
     * @author FadyAro
     *
     * 22.07.2018
     *
    
    /**
     * This contract is inherited later, to manage the owner, and use restriction for 
     * on some functions later in next contract(s)
     */
    contract Ownable {
    
        // the contract state variable as mentioned before
        address public owner;
    
        // whenever the ownership has been transferred log and event for auditing on the blockchain
        event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
    
        constructor() public {
            owner = msg.sender;
        }
    
        // this modifier when added to a function will make sure that the caller the owner
        // of the deployed contract
        modifier onlyOwner() {
            require(msg.sender == owner);
            _;
        }
    
        // a function to transfer the ownership to another account (address)
        // trigger the event later on (emit ...)
        function transferOwnership(address newOwner) public onlyOwner {
            require(newOwner != address(0));
            emit OwnershipTransferred(owner, newOwner);
            owner = newOwner;
        }
    }
    
    ...... // for briefing
    
    /**
     * This contract will run the EtherDrop logic
     */
    contract EtherDrop is Pausable {
    
        /*
         * lotto ticket price is 2e16 = 0.02 ETH
         */
        uint constant PRICE_WEI = 2e16;
    
        ...... // for briefing
    
        /*
         * this event is when we have a new winner
         * it is as well a new round start => (round + 1)
         */
        event NewWinner(address addr, uint round, uint place, uint value, uint price);
    
        struct history {
    
            /*
             * user black listed comment
             */
            uint blacklist;
    
            /*
             * user rounds subscriptions number
             */
            uint size;
    
            /*
             * array of subscribed rounds indexes
             */
            uint[] rounds;
    
            ...... // for briefing
        }
    
        /*
         * active subscription queue
         */
        address[] private _queue;
    
        /*
         * winners history
         */
        address[] private _winners;
    
        ...... // for briefing
    
        /*
         * active round queue pointer
         */
        uint public _counter;
    
        /*
         * allowed collectibles
         */
        uint private _collectibles = 0;
    
        /*
         * users history mapping
         */
        mapping(address => history) private _history;
    
        /**
         * get current round details
         */
        function currentRound() public view returns 
                 (uint round, uint counter, uint round_users, uint price) {
            return (_round, _counter, QMAX, PRICE_WEI);
        }
    
        /**
         * get round stats by index
         */
        function roundStats(uint index) public view returns 
                 (uint round, address winner, uint position, uint block_no) {
            return (index, _winners[index], _positions[index], _blocks[index]);
        }
    
       ...... // for briefing
    
        /**
         * round user subscription
         */
        function() public payable whenNotPaused {
            /*
             * check subscription price
             */
            require(msg.value >= PRICE_WEI, 'Insufficient Ether');
    
            /*
             * start round ahead: on QUEUE_MAX + 1
             * draw result
             */
            if (_counter == QMAX) {
    
                uint r = DMAX;
    
                uint winpos = 0;
    
                _blocks.push(block.number);
    
                // derive a random winning position
                bytes32 _a = blockhash(block.number - 1);
    
                for (uint i = 31; i >= 1; i--) {
                    if (uint8(_a[i]) >= 48 && uint8(_a[i]) <= 57) {
                        winpos = 10 * winpos + (uint8(_a[i]) - 48);
                        if (--r == 0) break;
                    }
                }
    
                _positions.push(winpos);
    
                /*
                 * post out winner rewards
                 */
                uint _reward = (QMAX * PRICE_WEI * 90) / 100;
                address _winner = _queue[winpos];
    
                _winners.push(_winner);
                _winner.transfer(_reward);
    
                ...... // for briefing
    
                /*
                 * log the win event: winpos is the proof, history trackable
                 */
                emit NewWinner(_winner, _round, winpos, h.values[h.size - 1], _reward);
    
                ...... // for briefing
    
                /*
                 * reset counter
                 */
                _counter = 0;
    
                /*
                 * increment round
                 */
                _round++;
            }
    
            h = _history[msg.sender];
    
            /*
             * user is not allowed to subscribe twice
             */
            require(h.size == 0 || h.rounds[h.size - 1] != _round, 'Already In Round');
    
            /*
             * create user subscription: N.B. places[_round] is the result proof
             */
            h.size++;
            h.rounds.push(_round);
            h.places.push(_counter);
            h.values.push(msg.value);
            h.prices.push(0);
    
            ...... // for briefing
    
            _counter++;
        }
    
        ...... // for briefing
         
    }

    阅读/分析注释函数后,让我们关注 function() public payable whenNotPaused

    function() 是函数关键字
    这个函数与其他函数不同,它没有名字
    它调用合约的 fallback 函数
    这意味着当调用合约时没有指定函数,如果找到,它将转到 fallback 函数

    public 意味着函数可以通过交易从合约外部执行

    payable 是一个必需的关键字,使函数能够 接受以太币 -> 到合约余额

  • fallback 函数中,您可能会注意到许多未赋值的变量,请参阅 以太坊文档 以了解全局变量和交易属性...
    • msg.value 包含交易发送的以太币数量(到合约)
    • msg.sender 包含发送者地址
msg.data (bytes): complete calldata
msg.gas (uint): remaining gas - deprecated in version 0.4.21 and to be replaced by gasleft()
msg.sender (address): sender of the message (current call)
msg.sig (bytes4): first four bytes of the calldata (i.e. function identifier)
msg.value (uint): number of wei sent with the message

_winner.transfer(_reward); _winner 是一个 address 类型的变量,因此 transfer 函数适用于将 _reward 转移到 address,其中 _reward 是一个大小为 256 字节的 uint,以 WEI 为单位

所以 EtherDrop 合约将按以下方式工作

  1. 在同一轮中,每个地址只允许一次订阅,否则交易将被撤销
        // checkout the history struct  { } 
        // get user history, _history is a more like a hashmap<address, history>
        h = _history[msg.sender];
        
        // this is a double check is the address or user has enter the same round before 
        require(h.size == 0 || h.rounds[h.size - 1] != _round, 'Already In Round');
  2. 在一轮中,订阅价格为 0.02 以太币(或 2e16 wei)
  3. 当队列满时,在下一轮开始时选择获胜者
  4. 从即将到来的区块中选择用户

每个区块都有一个哈希,即将到来的区块哈希将用作随机性的来源
将派生出一个介于 1 和 1000 之间的数字。
_queue[winpos] 将选择获胜者并发送其奖励

使用 truffle 编译合约

在命令行终端中 [确保您已安装 truffle]
> truffle compile (这将编译合约并显示错误/警告...)

注意“writing artifacts to .\build\contracts”,这将写入输出 *.json 合约构建规范,稍后在 Web 客户端 JS 中使用
以与区块链上的合约交互...

[ B ]

要使用 truffle 部署 SmartContract,应将其添加到 ./migrations./2_deploy_contracts.js

let EtherDrop = artifacts.require("EtherDrop"); // read the contract EtherDrop.sol
module.exports = function (deployer) {
    deployer.deploy(EtherDrop);                 // direct deployment instruction
};

现在在根文件夹中,注意 truffle.js 文件中的内容

module.exports = {
  networks: {
    development: {
      host: "127.0.0.1",    // where (which node) to deploy the contract
      port: 7545,           // the node port
      gas: 20500000,        // if you want to set network gas block tx limit Current 
                            // Ropsten gas limit. See https://ropsten.etherscan.io/block/3141628
      gasPrice: 1100000000, // if you want to change default network gas price 1.1 GWei - based on 
                            // the lower end of current txs getting into blocks currently on Ropsten.
      network_id: "5777"    // this is the network id (in case of Ganache truffle it is 5777 
                            // as in the screenshot)
    }
  }
};

现在让我们将合约部署到区块链(测试本地 ganache)

在命令行中进入 migrations 文件夹
> cd migrations

> truffle migrate

请注意合约地址(@ Deploying EtherDrop... 0x7ac26cc...)。

这是彩票合约地址(当然是在本地测试网络上)。

参与者将在此处发送 0.02 ETH 以订阅一轮(稍后在 Web UI 中)

您会在 ganache 日志中看到合约部署交易

[ C ]

在此步骤中,我们将讨论 Web 客户端。

因为名字是 EtherDrop,所以它是一个“Drop”,当回合订阅满时它将被填满。

我使用了 Materializecss,一个基于 Material Design 的现代响应式前端框架。

请访问那里了解更多关于它们的布局、网格、列、行、容器和样式...

我们不会专注于 HTML 设计,让我们跳到 Web3 和 Js 部分,主要是我们与智能合约和事件交互的地方。

与区块链交互需要 web3 提供程序,而大多数当前浏览器都不支持。

为此,您可以在 chrome 上安装 Metamask 并将其指向 localhost

在网络设置中使用 ganache,并使用助记词导入钱包

由 ganache 提供(在账户部分)

现在让我们检查 js 侧

打开 src/js/ed-client.js

    ... // brief
    
    // check for web3 provider
    initWeb3: () => {
        if (typeof web3 !== 'undefined') {
            BetClient.web3Provider = web3.currentProvider;
        } else {
            if (BetClient.onNoDefaultWeb3Provider) {
                setTimeout(BetClient.onNoDefaultWeb3Provider, 3000);
            }
            // if no web3 found or metamask just use for the infura
            let infura = 'https://mainnet.infura.io/plnAtKGtcoxBtY9UpS4b';
            BetClient.web3Provider = new Web3.providers.HttpProvider(infura);
        }
        web3 = new Web3(BetClient.web3Provider);
        return BetClient.initContract();
    },

    initContract: () => {
        // load the generated in build.. EtherDrop.Json
        dbg(`loading ${BetClient.jsonFile}`);
        $.getJSON(BetClient.jsonFile, (data) => {
            dbg('got abi');
            BetClient.contracts.EtherDrop = TruffleContract(data);
            BetClient.contracts.EtherDrop.setProvider(BetClient.web3Provider);
            // watch out the address from the network id (1 on mainnet)
            BetClient.address = data.networks[5777].address;
            if (BetClient.onContractLoaded) {
                BetClient.onContractLoaded();
            }
            BetClient.listen();
        });
    },

    // example of read the EtherDrop Current Round details
    loadRoundInfo: () => {
        BetClient.contracts.EtherDrop
            .deployed()
            .then((instance) => {
                // note that the instance is the contract EtherDrop itself
                // having in (EtherDrop.sol) the function currentRound()
                // that will return the round no, the counter, the price etc ...
                return instance.currentRound.call();
            })
            .then(function (result) {
                if (BetClient.onLoadRoundInfo)
                    // here we got the result[] of BigNumber uint256
                    // we may convert them to [] of Numbers
                    // and load them to the UI Presenter on callback
                    BetClient.onLoadRoundInfo($.map(result, (r) => {
                        return r.toNumber();
                    }));
            })
            .catch((e) => BetClient.raiseError(e, 'loadRoundInfo'));
    },

    ... // brief 


    // this is the payment part in case to be handler by Metamask or Web3 Browser
    participate: (amount) => {
        
        // get a loaded account from Metamask, or trustwallet 
        web3.eth.getAccounts((error, accounts) => {
            if (error) {
                BetClient.raiseError(error, 'Participate');
            } else {
               
                // use the account to do the transaction
                dbg(`accounts: ${JSON.stringify(accounts)}`);
                if (accounts.length === 0) {
                    BetClient.raiseError('No Accounts', 'Participate');
                } else {
                    BetClient.contracts.EtherDrop
                        .deployed()
                        .then((instance) => {
                            return instance.sendTransaction({
                                from: accounts[0],
                                value: amount // 0.02 ETH
                            }).then(function (result) {
                                dbg(JSON.stringify(result));
                            });
                        })
                        .catch((e) => BetClient.raiseError(e, 'Participate'));
                }
            }
        });
    },

    // listen to the contract events: New Winner, New Participation
    listen: () => {
        BetClient.contracts.EtherDrop
            .deployed()
            .then((instance) => {
                return instance; // the contract instance
            })
            .then((result) => {

                // blockchain read filtering from the latest to pending block
                const options = {fromBlock: 'latest', toBlock: 'pending'};

                // register for the NewDropIn (new subscription)
                result.NewDropIn({}, options).watch(function (error, result) {
                    if (error)
                        BetClient.raiseError(error, 'NewDropIn');
                    else if (BetClient.onNewDropIn)
                        BetClient.onNewDropIn(result);
                });

                // register for the NewWinner (and it is a new round start same time)
                result.NewWinner({}, options).watch((error, result) => {
                    if (error)
                        BetClient.raiseError(error, 'NewWinner');
                    else if (BetClient.onNewWinner)
                        BetClient.onNewWinner(result);
                });
            })
            // handle errors
            .catch((e) => BetClient.raiseError(e, 'listen'));
    }
};

[ D ]

现在我们已经谈得够多了,让我们在本地运行网络服务器。

我们正在使用 nodeJs - lite server(在 package.json 依赖文件中定义),它使用 bs 插件(浏览器同步)。

我喜欢这个静态页面服务服务器,它在每个文件内容更改时同步网页。

对于轻量级 Web 应用开发很方便,请注意 bs-config.json

它告诉 lite-server 监视并提供 ./src./build/contracts 下的文件(其中包含 EtherDrop.json

{
  "server": {
    "baseDir": ["./src", "./build/contracts"]
  }
} 

是的,现在让我们运行服务器

在命令行终端中键入

> npm run dev

成功!DApp 将在您的默认浏览器中启动,在 Chrome 中打开它。

确保 Metamask 已按照前面提到的正确配置!恭喜!

[ E ]

当它部署在主网时 [网络 ID 为 1]。

在文件末尾的 EtherDrop.Json 中,您需要将网络 ID 从 5777 替换为 1。
此外,将创建交易哈希和合约地址替换为相应的(已部署的实时合约,例如,来自 remix IDE)。

让我们看看在 Etherscan.io 上部署的智能合约

https://etherscan.io/address/0x81b1ff50d5bca9150700e7265f7216e65c8936e6

  • 单击 transactions 浏览在我们的 EtherDrop 智能合约上完成的交易。
    我们可以看到一笔交易失败了,因为发送方没有包含足够的 GAS :(
  • 点击 Code 查看已验证的合约源代码(二进制编译验证)

关注点

此合约正处于其第一个版本,在下一个 Drop/回合中,UI 将更新,将添加更多功能,并且合约将迁移并部署一个新合约,其中包括优化、性能增强和许多即将到来的预防措施。;)

加入并保持更新!比特币万岁,以太坊万岁!

谢谢!

Etherdrop.app

© . All rights reserved.