Skip to content

Latest commit

 

History

History
279 lines (245 loc) · 12 KB

README.md

File metadata and controls

279 lines (245 loc) · 12 KB

以太坊开发学习-Solidity

开发语言及工具:

  • 合约开发语言: Solidity
  • 合约编译器: solc
  • front-end: web3.js vue element
  • back-end: truffle
  • ganache-cli

deploy

进入docker环境

  • docker-compose up -d 利用docker部署node环境
  • docker exec -it node /bin/sh 进入容器终端

前期工作

  1. 启动ganache ganache-cli -h 0.0.0.0
  • -h 或 --hostname 设置监听主机,默认值同NodeJS的server.listen()
  • -g 设定Gas价格,默认值为20000000000
  • -p 设置监听端口,默认值为8545
  1. 重新开一个终端窗口进入容器,执行truffle console
  • compile 编译合约 --all 重新编译全部合约
  • migrate 部署合约 --reset 重新部署全部合约

Solidity练习-植物大战僵尸实例

本文只把跟重要的知识点相关的代码记录下来,方便加深记忆,对应的完整代码在contracts目录下。

Chapter1

建立一个名为ZombieFactory的智能合约,并创建个16位的DNA。

pragma solidity >=0.5.0 <0.6.0;  // 指定solidity版本
contract ZombieFactory {  // 创建一个僵尸工厂
    uint dnaDigits = 16; // uint实际上等于uint256,你可以用更少的位声明uint8,uint16,uint32等
    uint dnaModulus = 10 ** dnaDigits;  // 为了确保僵尸的DNA为16位字符,等于10^16
}

创建个僵尸的结构体和用于存储僵尸的数组.

struct Zombie {
    string name;
    uint dna;
}

Zombie[] public zombies;

新建一个用于创建僵尸的函数,为了合约的安全起见,应该将该函数设置为私有。storage的变量是永久储存在区块链变量,在函数外部声明的全局变量;而memory变量则是临时的区块链变量,当外部函数对某合约的调用完成时,内存型的变量将被移除,好比计算机的磁盘和RAM的区别。

function _createZombie(string memory _name, uint _dna) private { 
    zombies.push(Zombie(_name, _dna));  //填充数组
}

在solidity中函数声明应该包含返回类型,view只能读取数据保证不修改状态,pure承诺不读取或修改状态;定义一个随机数rand,并利用keccak256哈希计算来加密DNA,最后rand % dnaModulus,以确保DNA为16位。

function _generateRandomDna(string memory _name, uint _dna) private view returns (uint) {
    uint rand = uint(keccak256(abi.encodePacked(_str)));
    return rand % dnaModulus;
}

创建个可见性为public的函数,参数为_name,并分别调用_generateRandomDna和_createZombie。

function createRandomZombie(string memory _name) public {
    uint randDna = _generateRandomDna(_name);
    _createZombie(_name, randDna);
}

event是能方便调用以太坊虚拟机日志功能的接口,是将结果相应给前端的一种方式,event在合约中可以被继承。声明一个名为NewZombie事件,修改_createZombie函数,生成zombie并保留id,获取方式为zombies数组长度减去1,最后调用事件。

event NewZombie(uint zombieId, string name, uint dna);  // 声明一个名为"NewZombie"的事件
uint id = zombies.push(Zombie(_name, _dna)) - 1;
emit NewZombie(id, _name, _dna);  // 调用事件

web3.js中,提供了响应事件的方法,如下:

var event = myContract.transfer(function(e, r) {
    console.log('Event are as following:-------');
    for (let [k,v] in r.entries()) {
        console.log(k + ':' + v);
    }
    console.log('Event ending-------');
});
event.stopWatching(); // 终止事件监听

Chapter2

address:地址类型存储一个20字节,每个账号都有一个地址,你可以将其视为银行账号。它是你的唯一标识符,例如:0x0cE446255506E92DF41614C46F1d6df9Cc969183。在本节测试中,你需要声明一个mapping(映射)数据结构,key值是addressvalue为uint,映射是一种存储有组织数据的方法。

mapping (uint => address) public zombieToOwner;

在solidity中,存在一些全局变量;其中一个就是msg.sender,它指的是当前调用者(或智能合约中)的address。

zombieToOwner[id] = msg.sender;

现在为了游戏的可玩性,我们要限制玩家调用createRandomZombie函数来创建僵尸,所以我们要使用到require,require使得函数在执行过程中当不满足条件时,停止执行。

require(ownerZombieCount[msg.sender] == 0);

solidity中也引入了继承(Inheritance)的概念,可以将代码和逻辑拆分到不同的合约中去,便于管理,合约的继承用is关键字。在resource目录下创建个zombiefeeding.sol文件

import "./zombiefactory.sol";
contract ZombieFeeding is ZombieFactory {}

Chapter3

solidity封装了两种函数的调用方式,interal和external,interal类似于private,只是从该合约继承的合约也能访问;external类似于public,不同的是这些函数只能在合约外部调用,因为合约初始化未完成不能调用自身函数。

// external
contract A{
    function f(){}
}
contract B{
    //以`external`的方式调用另一合约中的函数
    function callExternal(A a){
        a.f();
    }
}
// internal
contract A{
    //默认是public函数
    function internalFunc() internal{}

    function callFunc(){
        //以`internal`的方式调用函数
        internalFunc();
    }
}
contract B is A{
    //子合约中调用
    function callFunc(){
        internalFunc();
    }
}

函数modify可以用来轻易改变一个函数的行为,比如用在函数前检查某种前置条件。修改器是一种合约属性,可以被继承,同时还可以被派生的合约重写。

pragma solidity ^0.5.0;
contract Ownable {
    address public owner = msg.sender;
    // 限制只有创建者才能访问
    modifier onlyOwner(address _newOwner) {
        if (msg.sender != _newOwner) thorw;
        _;
    }
    function changeOwner(address _newOwner) onlyOwner (_newOwner) {
        return 1;
    }
}

如果同一个函数有多个修改器,他们之间的空格隔开,修饰器会依次执行。

gas 合约一经创建,每笔交易都会收取一定数量的gas,目的是限制执行交易所需要的工作量和交易支付手续费。EVM执行交易时,gas将按照特定的规则逐渐耗尽。
使用struct结构体来节省gas的使用。通常使用uint8,uint16,uint32,etc;没有任何好处,因为uint无论大小如何,solidity都会保存256位存储空间。但是struct不一样,它会将这些变量打包在一起减少存储量。For example:

struct MiniMe {
    uint32 a;
    uint32 b;
    uint c;
}

变量的数据位置有三种类型,memory,storage,calldata。前两种已经介绍过,最后一种数据位置比较特殊,一般只有外部函数的参数(不包括返回参数)被强制指定为calldata。这种数据位置是只读的,不会持久化到区块链。

// 将memory于数组一起使用,在函数内部创建新的数组
function getArray() external pure returns(uint[] memory) {
    uint[] memory values = new uint[](3);  // memory 数组必须声明长度,后期solidity版本可能会修改
    values[0] = 1;
    values[1] = 2;
    values[2] = 3;
}

Chapter4

至此我们来回顾一下,之前介绍的函数状态修饰符。view函数不会保存或更改数据;pure函数不仅不会将数据保存在区块链,而且也不会读取链上的任何数据,而且从external外部访问,这两种调用方式都不会消耗gas(如果从另一个函数internal内部调用就要消耗gas)。

msg.value的值表示是向合同发送多少ether。payable标识的函数即可接受ether,并会把ether存在当前合约。例如:

contract GetPaid is Ownable {
    function withdraw() external onluOwner {
        address payable _owner = address(uint160(owner()));
        _owner.transfer(address(this).balance);  // address(this).balance 返回合同中存储的金额
    }
    // 同样可以使用转账将金额发送到任何以太坊地址
    function transfer() external onluOwner {
        uint itemFee = 0.001 ether;
        msg.sender.transfer(msg.value - itemFee);  
    }
}

keccak256(abi.encodePacked(noe, msg.sender, randNoce)) 用于将给定的参数打包,可用于生成随机字符。

Chapter5

创建个ERC721合约标准,接口定义如下:

  • 关于事件
    /* 该事件在代币被转手时得以触发。当代币所有权从某个用户转移到另个用户手上时,该事件被广播,
     * 其详细说明哪个账号发送该代币,哪个账户接收该代币,以及哪个代币被转移 
     */
    event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId);

    /* 该事件在某个用户授权从另一个用户获得代币的所有权时(即当授权被执行时)得以触发。
     * 其详细说明了哪个账户目前拥有该金币,哪个账户或许在未来获得该代币被授权转移其所有权 
     */
    event Approval(address indexed _owner, address indexed _approved, uint256 indexed _tokenId);
  • 关于方法
    //获取一个地址,返回改地址拥有的token数量
    function balanceOf(address _owner) external view returns (uint256){};
    //返回tokenid代币持有者的地址
    function ownerOf(uint256 _tokenId) external view returns (address){};
    //授予另一个实体代表所有者转移代币的权利
    function approve(address _approved, uint256 _tokenId) external payable{}; 
    //转移代币的下一个方法就是使用该函数。让代币所有者将其代币发送给另一个用户。 然而只有收款账户事先被打款账户授予获得代币的权利,转账才能开始 
    function transferFrom(address _from, address _to, uint256 _tokenId) external payable{}; 
}

assert() 用法与require一样,如果结果为false,它将会抛出错误。不同的是assert不会退还gas。因此,assert适合处理代码出现严重错误的情况下使用,比如(uint溢出)。For example:

function add(uint256 a, uint256 b)internal pure returns {
    uint256 c = a + b;
    assert(c >= a);
    return c;
}

labary与contract类似,但它们的目的是重用代码,不需要实例化,直接通过库.函数直接访问。使用using A for B来附着库A里定义的函数制定类型B。

pragma solidity ^0.4.0;
libary Set {
    struct Data {
        mapping(uint => bool) flags;
    }
    
    function contains(Data storage self, uint val) returns(bool) {
        return self.flags[val];
    }
}
contract LibaryUsingFor {
    using Set for Set.Data;  // 将Set附着到Set.Data上 能自动将调用对象做为第一个参数
    Set.Data data;
    function call() returns(bool) {
        data.flags[2] = true;
        return data.contains(2);  // 相当于data.contains(data, 2)
    }
}

Chapter6

// 安装web3
//using npm
npm install web3 
// using bower 
bower install web3

MetaMask是一个开源的以太坊钱包,可以存储个人私钥,在chrome扩展程序下载。

要使用智能合约必须先要从区块链中获取到合约实例,获取合约实例需要用到ABI和address(合约地址)。

call用于view和pure函数,它仅在本地节点上运行,不会在区块链上进行交易,不会消耗gas。 例如:myContract.methods.myMethod(123).call()

send会改变链上的数据,它适用于非view和pure函数,会产生gas费用,例如:myContract.methods`.myMethod(123).send()