以太坊Dapp开发全过程(solidity)

继上篇用php70行代码获取所有以太坊区块链应用代码,获取到以太坊dapp的solidity代码,除了用mythril工具扫描出安全问题,还是得深入分析代码逻辑。然而solidity语法有些不明白的地方,故借着loomnetwork的cryptozombies游戏 学习下用solidity开发区块链的全过程,在此总结分享一下

  • solidity文件扩展名为.sol,每条语句均使用分号结束
  • solidity开头应该声明版本,单个sol文件中可以有多个contract(最后一个为主contract, 其他contract当作类使用),可以通过import引入其他sol文件
  • 继承语法:contract ZombieFactory is Ownable, xx 多重继承
  • 函数返回值,注意returns 和 return两个位置
  • mapping相当于字典,address为地址类型,msg.sender为当前执行合约人的address
  • require函数在条件不符合时会终止执行
pragma solidity ^0.4.18;

import 'zeppelin-solidity/contracts/math/SafeMath.sol';
import 'zeppelin-solidity/contracts/ownership/Ownable.sol';

contract ZombieFactory is Ownable {

  using SafeMath for uint256;

  event NewZombie(uint zombieId, string name, uint dna);//声明事件

  uint dnaDigits = 16;
  uint dnaModulus = 10 ** dnaDigits;
  uint cooldownTime = 1 days;

  struct Zombie {
    string name;
    uint dna;
    uint32 level;
    uint32 readyTime;
    uint16 winCount;
    uint16 lossCount;
  }

  Zombie[] public zombies;

  mapping (uint => address) public zombieToOwner;
  mapping (address => uint) ownerZombieCount;

  function _createZombie(string _name, uint _dna) internal {
    uint id = zombies.push(Zombie(_name, _dna, 1, uint32(now + cooldownTime), 0, 0)) - 1;
    zombieToOwner[id] = msg.sender;
    ownerZombieCount[msg.sender] = ownerZombieCount[msg.sender].add(1);
    NewZombie(id, _name, _dna);//发送事件
  }

  function _generateRandomDna(string _str) private view returns (uint) {
    uint rand = uint(keccak256(_str));
    return rand % dnaModulus;
  }

  function createRandomZombie(string _name) public {
    require(ownerZombieCount[msg.sender] == 0);
    uint randDna = _generateRandomDna(_name);
    randDna = randDna - randDna % 100;
    _createZombie(_name, randDna);
  }

}
  • 以太坊上的dapp需要花费gas(以太币)来运行(pow工作量证明机制,gas付给运行以太坊节点),一个 DApp 收取多少 gas 取决于功能逻辑的复杂程度。每个操作背后,都在计算完成这个操作所需要的计算资源,(比如,存储数据就比做个加法运算贵得多), 一次操作所需要花费的 gas 等于这个操作背后的所有运算花销的总和。如果你使用侧链,倒是不一定需要付费,比如在 Loom Network 上构建的 CryptoZombies 就免费
  • now返回32位时间戳(自1970年1月1日以来经过的秒数),2038年会产生溢出。return (now >= (lastUpdated + 5 minutes)),判断时间过去5分钟
  • 存储:函数之外的变量(状态变量)均为storage变量,会永久存储在区块链上,操作他们需要花费gas;函数内部变量为memory临时变量,也可以显式声明为storage
  • 整数使用uint256(uint), uint32, uint16,存在overflow和underflow问题,使用safemath库解决。++, --操作会出现溢出问题。
  • uint、uint8放在struct中可以节省空间,当 uint 定义在一个 struct 中的时候,尽量使用最小的整数子类型以节约空间。 并且把同样类型的变量放一起(即在 struct 中将把变量按照类型依次放置),这样 Solidity 可以将存储空间最小化
  • safemath库分uint256, uint32等版本,可以定义在一个safemath.sol文件中,合约中使用库using SafeMath for uint256;
  • using SafeMath32 for uint32; using SafeMath16 for uint16;只需要复制library SafeMath,相应的更改名称及参数类型
pragma solidity ^0.4.11;


/**
 * @title SafeMath
 * @dev Math operations with safety checks that throw on error
 */
library SafeMath {
  function mul(uint256 a, uint256 b) internal returns (uint256) {
    uint256 c = a * b;
    assert(a == 0 || c / a == b);
    return c;
  }

  function div(uint256 a, uint256 b) internal returns (uint256) {
    // assert(b > 0); // Solidity automatically throws when dividing by 0
    uint256 c = a / b;
    // assert(a == b * c + a % b); // There is no case in which this doesn't hold
    return c;
  }

  function sub(uint256 a, uint256 b) internal returns (uint256) {
    assert(b <= a);
    return a - b;
  }

  function add(uint256 a, uint256 b) internal returns (uint256) {
    uint256 c = a + b;
    assert(c >= a);
    return c;
  }
}
  • 函数修饰符有public,private,internal,external,view,pure,payable类型。public为任何其他合约可见的;private为仅本文件合约可用;internal为本合约及子合约可用;external为只能外部合约使用;view为仅读取合约数据,不消耗gas的代码;pure为仅产生数据不操作合约数据的代码,也不消耗gas;payable函数需要前端支付gas才能执行。函数默认为public,变量默认为internal
  • payable函数,向用户收取以太币
CryptoZombies.methods.levelUp(zombieId)
.send({ from: userAccount, value: web3js.utils.toWei("0.001","ether") })
  • 调用其他合约,需要定义接口Interface,仅声明函数。调用需要获传入合约地址
contract NumberInterface {
  function getNum(address _myAddress) public view returns (uint);
}      address NumberInterfaceAddress = 0xab38...;
  // ^ 这是FavoriteNumber合约在以太坊上的地址
  NumberInterface numberContract = NumberInterface(NumberInterfaceAddress);
  • 有些操作只能供合约owner调用,可以建个Ownable (OpenZeppelin库) 合约供其他合约继承,构造函数Ownable在合约初次部署时被记录。增加onlyOwner函数modifier,提供便捷的require判断,函数modifier可以传参数,_;为交接执行权限到函数本身
pragma solidity ^0.4.19;
/**
 * @title Ownable
 * @dev The Ownable contract has an owner address, and provides basic authorization control
 * functions, this simplifies the implementation of "user permissions".
 */
contract Ownable {
  address public owner;

  event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

  /**
   * @dev The Ownable constructor sets the original `owner` of the contract to the sender
   * account.
   */
  function Ownable() public {
    owner = msg.sender;
  }


  /**
   * @dev Throws if called by any account other than the owner.
   */
  modifier onlyOwner() {
    require(msg.sender == owner);
    _;
  }


  /**
   * @dev Allows the current owner to transfer control of the contract to a newOwner.
   * @param newOwner The address to transfer ownership to.
   */
  function transferOwnership(address newOwner) public onlyOwner {
    require(newOwner != address(0));
    OwnershipTransferred(owner, newOwner);
    owner = newOwner;
  }

}
  • uint[] public intarray;声明公开的动态数组。solidity自动创建getter方法,前端可以通过intarray(0)获取值;intarray.push(1),添加元素。内存数组必须 用长度参数创建uint[] memory values = new uint[](3);
  • sha3散列函数keccake256(),用散列值进行判断字符串相等;用散列值生成伪随机数,https://blog.positive.com/predicting-random-numbers-in-ethereum-smart-contracts-e5358c6b8620
  • msg.sender.transfer(msg.value - itemFee);向执行合约的人退回多余的以太币。this.balance代表当前合约存储了多少以太币,下面是合约提现函数
function withdraw() external onlyOwner {
    owner.transfer(this.balance);
  }

  • 以太坊的代币比如ERC20 代币,所有合约遵循相同规则,即它实现了所有其他代币合约共享的一组标准函数,例如 transfer(address _to, uint256 _value) 和 balanceOf(address _owner)
  • 在智能合约内部,通常有一个映射, mapping(address => uint256) balances,用于追踪每个地址还有多少余额。所以基本上一个代币只是一个追踪谁拥有多少该代币的合约,和一些可以让那些用户将他们的代币转移到其他地址的函数。一个例子就是交易所。 当交易所添加一个新的 ERC20 代币时,实际上它只需要添加与之对话的另一个智能合约。 用户可以让那个合约将代币发送到交易所的钱包地址,然后交易所可以让合约在用户要求取款时将代币发送回给他们
  • 有另一个代币标准更适合如 CryptoZombies 这样的加密收藏品——它们被称为ERC721 代币。ERC721 代币是不能互换的,因为每个代币都被认为是唯一且不可分割的。 你只能以整个单位交易它们,并且每个单位都有唯一的 ID,每个单位都有特定属性。下面是ERC721约定,需自行实现函数定义
contract ERC721 {
  event Transfer(address indexed _from, address indexed _to, uint256 _tokenId);
  event Approval(address indexed _owner, address indexed _approved, uint256 _tokenId);

  function balanceOf(address _owner) public view returns (uint256 _balance);
  function ownerOf(uint256 _tokenId) public view returns (address _owner);
  function transfer(address _to, uint256 _tokenId) public;
  function approve(address _to, uint256 _tokenId) public;
  function takeOwnership(uint256 _tokenId) public;
}
  •  ERC721 规范有两种不同的方法来转移代币,直接transfer和先发出申请、然后接收人主动接收。transfer之后需要发送Transfer事件到前端,Approval事件也需要
function transfer(address _to, uint256 _tokenId) public;

function approve(address _to, uint256 _tokenId) public;
function takeOwnership(uint256 _tokenId) public;
  • 前端编写,以太坊节点接收json-rpc类协议,前端使用web3.js与节点交互。web3.js可设置infura和metamask作为服务提供者(另一层封装),开发者不需要自己搭建节点。
  • infura  var web3 = new Web3(new  Web3.providers.WebsocketProvider("wss://mainnet.infura.io/ws"));
  • 写入合约数据需要用户私钥,infura不如metamask方便。metamask是基于infura开发的浏览器插件,可以管理用户以太坊账号。Metamask 把它的 web3 提供者注入到浏览器的全局 JavaScript对象web3中。所以你的应用可以检查 web3 是否存在。若存在就使用 web3.currentProvider 作为它的提供者
window.addEventListener('load', function() {

  // 检查web3是否已经注入到(Mist/MetaMask)
  if (typeof web3 !== 'undefined') {
    // 使用 Mist/MetaMask 的提供者
    web3js = new Web3(web3.currentProvider);
  } else {
    // 处理用户没安装的情况, 比如显示一个消息
    // 告诉他们要安装 MetaMask 来使用我们的应用
  }

  // 现在你可以启动你的应用并自由访问 Web3.js:
  startApp()

})
  • 前端调用合约,var myContract = new web3js.eth.Contract(myABI, myContractAddress);参数为编译产生的ABI和部署后的合约地址  (较为麻烦,未来会使用ENS和swarm:http://blog.sina.com.cn/s/blog_6cd4a7350102yc91.html)
  • 调用合约函数有call和send两个函数,call调用不需要花费gas的函数
myContract.methods.myMethod(123).call()
myContract.methods.myMethod(123).send({ from: userAccount }).on("receipt", function(receipt) {}).on("error", function(error) {})
//public数组zombies
cryptoZombies.methods.zombies(id).call()
  • 从metamask中获取用户账号var userAccount = web3.eth.accounts[0]
var accountInterval = setInterval(function() {
  // 检查账户是否切换
  if (web3.eth.accounts[0] !== userAccount) {
    userAccount = web3.eth.accounts[0];
    // 调用一些方法来更新界面
    updateInterface();
  }
}, 100);
  • 获取合约数据, Web3.js 的 1.0 版使用的是 Promises 而不是回调函数
function startApp() {
        var cryptoZombiesAddress = "YOUR_CONTRACT_ADDRESS";
        cryptoZombies = new web3js.eth.Contract(cryptoZombiesABI, cryptoZombiesAddress);

        var accountInterval = setInterval(function() {
          // Check if account has changed
          if (web3.eth.accounts[0] !== userAccount) {
            userAccount = web3.eth.accounts[0];
            // Call a function to update the UI with the new account
            getZombiesByOwner(userAccount)
            .then(displayZombies);
          }
        }, 100);
      }

      function zombieToOwner(id) {
        return cryptoZombies.methods.zombieToOwner(id).call()
      }
  • event是合约用来发送事件,供前端web3.js来接受的,前端订阅事件( Web3.js 最新版1.0的,此版本使用了 WebSockets 来订阅事件,metamask暂不支持websocket,可使用infura provider)
为了筛选仅和当前用户相关的事件,我们的 Solidity 合约将必须使用 indexed 关键字,就像我们在 ERC721 实现中的Transfer 事件中那样:
event Transfer(address indexed _from, address indexed _to, uint256 _tokenId);
日志中存储的不同的索引事件就叫不同的主题。事件定义,event transfer(address indexed _from, address indexed _to, uint value)有三个主题,第一个主题为默认主题,即事件签名transfer(address,address,uint256),但如果是声明为anonymous的事件,则没有这个主题;另外两个indexed的参数也会分别形成两个主题,可以分别通过_from,_to主题来进行过滤。如果数组,包括字符串,字节数据做为索引参数,实际主题是对应值的Keccak-256哈希值
在这种情况下, 因为_from 和 _to 都是 indexed,这就意味着我们可以在前端事件监听中过滤事件
cryptoZombies.events.Transfer({ filter: { _to: userAccount } })
.on("data", function(event) {
  let data = event.returnValues;
  // 当前用户更新了一个僵尸!更新界面来显示
}).on('error', console.error);
  • 查询过去的事件
我们甚至可以用 getPastEvents 查询过去的事件,并用过滤器 fromBlock 和 toBlock 给 Solidity 一个事件日志的时间范围("block" 在这里代表以太坊区块编号):

cryptoZombies.getPastEvents("NewZombie", { fromBlock: 0, toBlock: 'latest' })
.then(function(events) {
  // events 是可以用来遍历的 `event` 对象 
  // 这段代码将返回给我们从开始以来创建的僵尸列表
});

因为你可以用这个方法来查询从最开始起的事件日志,这就有了一个非常有趣的用例: 用事件来作为一种更便宜的存储。
这里的短板是,事件不能从智能合约本身读取

  • ERC20规范
contract ERC20 {
    function totalSupply() constant returns (uint supply);
    function balanceOf( address who ) constant returns (uint value);
    function allowance( address owner, address spender ) constant returns (uint _allowance);//判断还可以转移多少币

    function transfer( address to, uint value) returns (bool ok);
    function transferFrom( address from, address to, uint value) returns (bool ok);
    function approve( address spender, uint value ) returns (bool ok);

    event Transfer( address indexed from, address indexed to, uint value);
    event Approval( address indexed owner, address indexed spender, uint value);
}

你可能感兴趣的:(以太坊)