Solidity Tutorial (2)

Contents

pragma solidity ^0.4.4;
/*
pragma:版本声明
Solidity:开发语言
0.4.4:当前合约的版本,0.4代表主版本,4代表修复bug的升级版本
^:代表向上兼容,0.4.4 ~ 0.4.9可以对我们当前的代码进行编译
*/
 
contract Person {
  uint _age;
  uint _height;
  address _owner; // 合约的拥有者
 
  constructor () public{
    _age = 29;
    _height = 180;
    _owner = msg.sender;
  }
 
  function owner() public constant returns (address){
    return _owner;
  }
 
  function getAge() public constant returns(uint) {
    return _age;
  }
 
  function getHeight() public constant returns(uint) {
    return _height;
  }
 
  function setAge(uint age) public {
    _age = age;
  }
 
  function setHeight(uint height) public {
    _height = height;
  }
 
  function kill() public {
    if(_owner == msg.sender){
      selfdestruct(_owner); // 摧毁拥有者的合约
    }
  }

注释1:Contract

Contract是一个内置的对象,上面的语法就类似于class Person(子类) extends Contract(父类),意味着Person继承于Contract。 对象内有其构造函数constructor,用以对实例进行初始化。 当合约部署的时候,就是产生一个合约的实例,来自同一个地址的相同合约只能部署一次,只有一个专有的合约地址。

注释2:状态变量

_age , _height都是状态变量,在Contract中就相当于其属性变量; 在 Solidity 中,有两个地方可以存储变量 —— storage以及memory。 Storage 变量是指永久存储在区块链中的变量。 Memory 变量则是临时的,当外部函数对某合约调用完成时,内存型变量即被移除。 状态变量(在函数之外声明的变量)默认为“storage”形式,并永久写入区块链;而在函数内部声明的变量默认是“memory”型的,它们函数调用结束后消失。 Storage 拿到的是引用/句柄/指针, memory 拿到的是一份拷贝。

注释3:函数和状态变量的可见性

因为Solidity有两种函数调用:

内部调用:不创建一个真实的EVM调用(也称为“消息调用”); 外部的调用:要创建一个真实的EMV调用, 在智能合约中,函数和状态变量的可见性可以分为四种, public , private , internal 和 external ,函数默认可见性是 public ,状态变量的默认可见性是 internal 。

public - (任意访问,作为合约接口)可以通过内部调用或通过消息调用。对公共状态变量而言,会有的自动访问限制符的函数生成。

private - (仅当前合约内)私有函数和状态变量仅仅在定义该合约中可见, 在派生的合约中不可见。

internal - (仅当前合约及所继承的合约)

这些函数和状态变量只能内部访问(即在当前合约或由它派生的合约),而不使用(关键字)this 。 external - (仅外部访问,也是合约接口)它们可以从其他合约调用, 也可以通过事务调用。外部函数f不能被内部调用(在内部也只能用外部访问方式访问,即 f()不执行,但this.f()执行)。

注释4:函数的限制访问

在Solidity中 constant 、 view 、 pure 三个函数修饰词的作用是告诉编译器,函数不改变/不读取状态变量,这样函数执行就可以不消耗gas了,因为不需要矿工来验证。

在Solidity v4.17之前,只有constant,后续版本将constant拆成了view和pure。view的作用和constant一模一样,可以读取状态变量但是不能改;pure则更为严格,pure修饰的函数不能改也不能读状态变量,只能操作函数内部变量,否则编译通不过。

注释5:msg.sender 和 selfdestruct()

在Contract中有一些全局变量和函数,在我们编写智能合约的过程中可以直接调用,比如上面的 msg.sender 和 selfdestruct()

msg的所有成员包括:

msg.sender :储存消息的发送者,即部署智能合约的账户地址 msg.value :发送的消息的数量 msg.gas :剩余的gas msg.data:完整的calldata msg.sig :呼叫数据的前4个字节

合约相关的方法:

selfdestruct ( address recipient ) :摧毁目前的合同,将资金送到给定的地址 suicide ( address recipient ):同上,是别名 this:指当前合约,明确转换为地址

全部的全局变量和函数有很多,包括:

  • Ether单元
  • 时间单位
  • 块相关
  • msg相关
  • tx相关
  • 当前时间戳
  • 错误处理
  • 数学和加密功能
  • 地址相关
  • 合约相关

继承

状态变量的继承:可以继承public和internal,但不能继承private

函数的继承:只能继承Public,不能继承internal和private

调用外部函数以及关键字payable


pragma solidity ^0.4.0;

contract InfoFeed {

    function info() payable returns (uint ret) { // 如果这里有payable,说明该函数外部调用的时候必须发送ether和gas
        return msg.value;
    }
}

contract Consumer {

    function deposit() payable returns (uint){
        return msg.value;
    }

    function left() constant returns (uint){
        return this.balance;
    }

    function callFeed(address addr) returns (uint) {
        return InfoFeed(addr).info.value(1).gas(8000)();  
        // 调用InfoFeed合约中的info函数,使用value()和fas()方法向InfoFeed合约发送ether
    }

首先调用deposit()为Consumer合约存入一定量的ether。然后调用callFeed()通过value(1)的方式,向InfoFeed合约的info()函数发送1ether。如果不先充值,由于合约余额为0,余额不足会报错Invalid opcode。

InfoFeed.info()函数,必须使用payable关键字,否则不能通过value()选项来接收ether。

如果被调用的合约不存在,或者是不包代码的帐户,或调用的合约产生了异常,或者gas不足,均会造成函数调用发生异常。 .info.value(1).gas(8000)只是本地设置发送的数额和gas值,真正执行调用的是其后的括号.info.value(1).gas(8000)()。

代码InfoFeed(addr)进行了一个显示的类型转换,声明了我们确定知道给定的地址是InfoFeed类型。所以这里并不会执行构造器的初始化。显示的类型强制转换,需要极度小心,不要尝试调用一个你不知道类型的合约。

如果被调用的合约源码并不事前知道,和它们交互会有潜在的风险。当前合约会将自己的控制权交给被调用的合约,而对方几乎可以做任何事。即使被调用的合约是继承自一个已知的父合约,但继承的子合约仅仅被要求正确实现了接口。合约的实现,可以是任意的内容,由此会有风险。另外,准备好处理调用你自己系统中的其它合约,可能在第一调用结果未返回之前就返回了调用的合约。某种程度上意味着,被调用的合约可以改变调用合约的状态变量(state variable)来标记当前的状态。如,写一个函数,只有当状态变量(state variables)的值有对应的改变时,才调用外部函数,这样你的合约就不会有可重入性漏洞。

Storage(引用传递)和Memory(值传递)

Solidity中有两种类型:值类型和引用类型 Solidity是静态类型的语言,有值类型和引用类型的区别。

如果一个变量是值类型,那么当把它的值传给另一个变量时,是复制值,对新变量的操作不会影响原来的变量;如果该变量是引用类型,那么当它传值给另一个变量时,则是把该值的地址复制给新的变量。这样操作新变量也会导致旧变量的改变。

值类型: 布尔类型(bool)、整型(int)、地址类型(address)、定长字节数组(bytes)、枚举类型(enums)、函数类型(function);

如果一个变量是值类型,给它赋值时永远是值传递!

引用类型: 字符串(string)、数组(array)、结构体(structs)、字典(mapping)、不定长字节数组(bytes)

如果一个变量是引用类型,给它赋值时可以是值,也可以是引用,这决定于该变量是Storage类型还是Memory类型。

关键字:Storage 和 Memory Storage 是把变量永久储存在区块链中,Memory 则是把变量临时放在内存中,当外部函数对某合约调用完成时,内存型变量即被移除。 你可以把它想象成存储在你电脑的硬盘或是RAM中数据的关系。

大多数时候你都用不到这些关键字,默认情况下 Solidity 会自动处理它们。

状态变量(在函数之外声明的变量)默认为“存储”形式,并永久写入区块链;而在函数内部声明的变量是“内存”型的,它们函数调用结束后消失。

通过指定引用类型变量的关键字,可以人为设置变量为storage或memory。

函数的引用类型参数是storage时,是引用传递;函数的引用类型参数是Memory时,是值传递;函数值类型参数永远是值传递。


contract SandwichFactory {
  struct Sandwich {
    string name;
    string status;
  }
  Sandwich[] sandwiches;
  function eatSandwich(uint _index) public {
    // Sandwich mySandwich = sandwiches[_index];
    /*
       看上去很直接,不过 Solidity 将会给出警告,告诉你应该明确在这里定义 `storage` 或者 `memory`。
       所以你应该明确定义 `storage`:
    */
    Sandwich storage mySandwich = sandwiches[_index];
    // 这样 `mySandwich` 是指向 `sandwiches[_index]`的指针在存储里,另外...
    mySandwich.status = "Eaten!";
    // 这将永久把 `sandwiches[_index]` 变为区块链上的存储,如果你只想要一个副本,可以使用`memory`:
    Sandwich memory anotherSandwich = sandwiches[_index + 1];
    // 这样 `anotherSandwich` 就仅仅是一个内存里的副本了
    // 另外
    anotherSandwich.status = "Eaten!";
    // 将仅仅修改临时变量,对 `sandwiches[_index + 1]` 没有任何影响
    // 不过你可以这样做:
    sandwiches[_index + 1] = anotherSandwich;
    // 如果你想把副本的改动保存回区块链存储
  }

接口和抽象合约

接口的存在就是为了合约之间的通信。

有两种实现方式:抽象合约 和 接口

  1. 抽象合约

抽象函数是没有函数体的的函数。如下:

pragma solidity ^0.4.0;

contract Feline {
    function utterance() returns (bytes32);
}

这样的合约不能通过编译,即使合约内也包含一些正常的函数。但它们可以做为基合约被继承。

pragma solidity ^0.4.0;

contract Feline {
    function utterance()
        returns (bytes32);
    function getContractName() returns (string){
        return "Feline";
    }
}

contract Cat is Feline {
    function utterance() returns (bytes32) {
        return "miaow";
    }
}

如果一个合约从一个抽象合约里继承,但却没实现所有函数,那么它也是一个抽象合约。

如何通过抽象合约实现接口功能?

如果contract B要使用contract A的方法或数据,本质上:

先定义一个抽象合约,让contract A继承于这个抽象合约; 把contract A中已经实现了的方法放入抽象合约中,solidity会自动把这个抽象合约视作接口; contract B通过contract A的地址来创建连接到contract A的接口实例; 调用contract A中的方法或读取数据;

  1. 接口

接口与抽象合约类似,与之不同的是,接口内没有任何函数是已实现的,同时还有如下限制:

不能继承其它合约,或接口。 不能定义构造器 不能定义变量 不能定义结构体 不能定义枚举类 接口基本上限制为合约ABI定义可以表示的内容,ABI和接口定义之间的转换应该是可能的,不会有任何信息丢失。

注意: 1、在两个.sol文件中都声明接口,或者两个合约写到一个.sol文件里,那就只要声明一次; 2、在一个合约中实现METHOD_A,该合同必须继承自接口interfaceContract; 3、在另一个合约中创建一个interfaceContract实例,该实例接受实现接口的合约的地址; 4、通过这个实例调用目标合约的方法,获取目标合约的数据;

实例:

被调用合约 InterfaceImpContract

pragma solidity ^0.4.16;

interface interfaceContract {
    function receiveApproval(address _from, uint256 _value, address _token, bytes _extraData);
}

contract InterfaceImplContract is interfaceContract {
    event Receive(address from, uint256 value, address token, bytes extraData);
    function receiveApproval(address _from, uint256 _value, address _token, bytes _extraData) {
        Receive(_from,_value,_token,_extraData);
    }
}

调用合约 RemoteContract

pragma solidity ^0.4.16;

interface interfaceContract {
    function receiveApproval(address _from, uint256 _value, address _token, bytes _extraData);
}

contract RemoteContract {
    function func(address _addr, uint _value) {
        //注意这里的_addr参数,需要填写tokenRecipient合约的地址。这里加载已经存在的智能合约。如何合约不存在会报错回滚。
        interfaceContract _interfaceContract = interfaceContract(_addr);
        _interfaceContract.receiveApproval(msg.sender, _value, address(this), "这是一些信息");
    }
}

addr,在下面的代码中,用来加载合约interfaceContract _interfaceContract = interfaceContract(_addr); 所以addr必须传递合约地址。并且这个合约地址是interfaceContract的实现类的合约地址。也就是第一步创建的InterfaceImplContract 合约的地址。

如果传递的_addr参数错误,调用失败。它将回滚所有已执行的功能。也就是这个方法会回滚。 这里部署时,只需部署RemoteContract 即可。不用管接口。接口只是为了声明。

事件、日志与交互(含实例)

事件是以太坊EVM提供的一种日志基础设施。事件可以用来做操作记录,存储为日志。也可以用来实现一些交互功能,比如通知UI,返回函数调用结果等。

总的来说:事件就是当区块链某个函数被调用或执行的时候,被触发从而被前端获取或者记录到日志中的对象。

1. 事件的实现

事件的实现是在合约对象中,分两步: 1、定义事件类型 2、实例化事件对象

代码:


pragma solidity ^0.4.19;
contract ZombieFactory {
    // 定义事件类型
    event NewZombie(uint zombieId, string name, uint dna);
    uint dnaDigits = 16;
    uint dnaModulus = 10 ** dnaDigits;
    struct Zombie {
        string name;
        uint dna;
    }
    Zombie[] public zombies;
    function _createZombie(string _name, uint _dna) private {
        uint id = zombies.push(Zombie(_name, _dna)) - 1;
        // 实例化事件对象
        NewZombie(id, _name, _dna);
    }

2. 事件与交互

我们在前端使用web3.js来与区块链进行交互。当智能合约中的函数被调用而更改了区块链中的数据后,前端如何实时进行相应的行为?

1、调用合约,生成一个可以访问公共函数和事件的合约对象; 2、监听事件,调用事件方法,异步获取事件返回的值,error或者result; 3、判断并执行相应的前端函数; 4、注意:在操作执行完成后,我们要记得调用event.stopWatching();来终止监听。

代码:

// 下面是调用合约的方式:
var abi = /* abi是由编译器生成的 */
var ZombieFactoryContract = web3.eth.contract(abi)
var contractAddress = /* 发布之后在以太坊上生成的合约地址 */
var ZombieFactory = ZombieFactoryContract.at(contractAddress)
// `ZombieFactory` 能访问公共的函数以及事件

// 监听 `NewZombie` 事件, 并且更新UI
var event = ZombieFactory.NewZombie(function(error, result) {
  if (error) return
  generateZombie(result.zombieId, result.name, result.dna)
})

// 获取 Zombie 的 dna, 更新图像
function generateZombie(id, name, dna) {
  // 新建一个Zombie的图像
  ...

3. 事件与日志

如上所说,事件是以太坊EVM提供的一种日志基础设施,日志是区块链中的一种特殊数据结构。

当定义的事件触发时,我们可以将事件存储到EVM的交易日志中,日志与合约关联,与合约的存储合并存入区块链中。只要某个区块可以访问,其相关的日志就可以访问。但在合约中,我们不能直接访问日志和事件数据(即便是创建日志的合约)。

web3.js监听事件,实际上是对EVM的交易日志的监听。

所以,当我们需要对事件日志进行条件性的过滤,即只在满足某些条件的情况下才执行前端的函数,要如何进行?

检索日志:indexed属性的使用

可以在事件参数上增加indexed属性,最多可以对三个参数增加这样的属性。加上这个属性,可以允许你在web3.js中通过对加了这个属性的参数进行值过滤,方式如下:

var event = myContract.transfer({value: "100"});

上面实现的是对value值为100的日志,过滤后的返回。

如果你想同时匹配多个值,还可以传入一个要匹配的数组。

var event = myContract.transfer({value: ["99","100","101"]});

增加了indexed的参数值会存到日志结构的Topic部分,便于快速查找。 未加indexed的参数值会存在data部分,成为原始日志。

需要注意的是,如果增加indexed属性的是数组类型(包括string和bytes),那么只会在Topic存储对应的数据的web3.sha3哈希值,将不会再存原始数据。因为Topic是用于快速查找的,不能存任意长度的数据,所以通过Topic实际存的是数组这种非固定长度数据哈希结果。要查找时,是将要查找内容哈希后与Topic内容进行匹配,但我们不能反推哈希结果,从而得不到原始值。

所以如果你要实现过滤还要获得原始值,那就不要把indexed加到string和bytes类型参数前面。

使用web3.js读取事件的完整例子

下面是一个使用以太坊提供的工具包web3.js访问事件的完整例子:


let Web3 = require('web3');
let web3;

if (typeof web3 !== 'undefined') {
    web3 = new Web3(web3.currentProvider);
} else {
    // set the provider you want from Web3.providers
    web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545"));
}

let from = web3.eth.accounts[0];

//编译合约
let source = "pragma solidity ^0.4.0;contract Transfer{ event transfer(address indexed _from, address indexed _to, uint indexed value); function deposit() payable { address current = this; uint value = msg.value; transfer(msg.sender, current, value); } function getBanlance() constant returns(uint) { return this.balance; } /* fallback function */ function(){}}";

let transferCompiled = web3.eth.compile.solidity(source);
console.log(transferCompiled);
console.log("ABI definition:");
console.log(transferCompiled["info"]["abiDefinition"]);

//得到合约对象
let abiDefinition = transferCompiled["info"]["abiDefinition"];
let transferContract = web3.eth.contract(abiDefinition);
 
//2. 部署合约
//2.1 获取合约的代码,部署时传递的就是合约编译后的二进制码
let deployCode = transferCompiled["code"];
//2.2 部署者的地址,当前取默认账户的第一个地址。
let deployeAddr = web3.eth.accounts[0];

//2.3 异步方式,部署合约
//警告,你不应该每次都部署合约,这里只是为了提供一个可以完全跑通的例子!!!
transferContract.new({
    data: deployCode,
    from: deployeAddr,
    gas: 1000000
}, function(err, myContract) {
    if (!err) {
        // 注意:这个回调会触发两次
        //一次是合约的交易哈希属性完成
        //另一次是在某个地址上完成部署
        // 通过判断是否有地址,来确认是第一次调用,还是第二次调用。
        if (!myContract.address) {
            console.log("contract deploy transaction hash: " + myContract.transactionHash) //部署合约的交易哈希值
            // 合约发布成功后,才能调用后续的方法
        } else {
            console.log("contract deploy address: " + myContract.address) // 合约的部署地址
            console.log("Current balance: " + myContract.getBanlance());
            var event = myContract.transfer();
            // 监听
            event.watch(function(error, result){
              console.log("Event are as following:-------");
              for(let key in result){
                console.log(key + " : " + result[key]);
              }
              console.log("Event ending-------");
            });
            //使用transaction方式调用,写入到区块链上
            myContract.deposit.sendTransaction({
                from: deployeAddr,
                value: 100,
                gas: 1000000
            }, function(err, result){
              console.log("Deposit status: " + err + " result: " + result);
              console.log("After deposit balance: " + myContract.getBanlance());
              //终止监听,注意这里要在回调里面,因为是异步执行的。
              event.stopWatching();
            });
        }
    }