solidity学习笔记(二)

solidity 合约文件结构

合约文件:

  • 版本申明
  • 合约主体:
    1. 状态变量
    2. 函数
    3. 结构类型
    4. 事件
    5. 函数修改器
  • 代码注释

例子:

  pragma solidity ^0.4.0;

  import "";

  contract Test {
    //状态变量
    uint a; 
    
    function setA(uint x) public {
      a = x;
      // 调用事件
      emit Set_A(x)
    }

    // 定义事件
    event Set_A(uint a);

    // 定义结构类型
    struct Pos {
      int lat;
      int lng;
    }

    //函数修改器
    modifier owner () {

    }

  }

整型 —int/uint

int 是有符号的整型,而uint是无符号的整型 uint8 表示无符号的8位整型。

整型的运算符:

  • 比较运算符: <= , < , == , != ,>= , >
  • 位运算:& , | ,^(异或) , ~(位取反)
  • 算数运算: + , - , 一元运算 ,…(其中 ** 表示取幂,>> 表示又移两位)
pragma solidity ^0.4.0;

contract Test {
  uint a;
  uint256 b = 20;
  uint256 c = 30;

  function testadd() public returns (int) {
    if(b>c){
      return b + c
    } else if(b == c){
      return b * c
    } else {
      return b >> 2;
      //结果是5 b>> 2 表示 20 / 2^2
    }
    
  }
}

常量 contant

  • 有理数和整形常量
  • 字符串常量
  • 十六进制常量
  • 地址常量

uint是指只支持固定的位数的变量,而constant 是支持任意精度的,所以有时候会运用到常量
并且常量的运算是不会有溢出的

pragma solidity ^0.4.0;

contract Test {
 
  function testLiterals() public constant returns (uint){
      return 1; // 这个 1 就是一个常量
      // return 1 + 1;
      // return 1.0 + 1e10;  有理数的常量
  }
  function testStringLiterals() public constant returns(string){
      return  "abc";  //字符串常量会被转换成一个字节数组或者是字符串数组
  }

  function testHexLiterals() public constant returns(bytes2,bytes1,bytes1){
    // 十六进制常量返回的是一个字节数组,十六进制常量是以hex开头的
      bytes2 a = hex"abcd";
      return (a,a[0],a[1]);  // "0xabcd","0xab","0xcd"
  }
}

contant 表示不会修改状态变量的常量,新版本也可以使用 view
是一种数据类型,比如数字 1 和 1.000 就是常量
字符串常量:就是字符串

地址类型

表示一个账户的地址(20字节)
地址类型成员包括: 获取账户余额:`balance` ,转移账户余额:` transfer(金额数目)`
pragma solidity ^0.4.16;

contract AddrTest {
    //当表示一个方法可以接受以太币时需要在函数声明的时候加上 payable
    function deposit() public payable {
        
    }
    
    function getBalance() public constant returns (uint) {
        return this.balance;
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7Xr2b3Ie-1572335194921)(.\img\1.png)]
实现转账功能

  pragma solidity ^0.4.16;

  contract AddrTest {
    //当表示一个方法可以接受以太币时需要在函数声明的时候加上 payable
    function deposit() public payable {
        
    }
    function getBalance() public constant returns (uint) {
        return this.balance;
    }
    function transferEther(address towho) public {
      //10 表示转10个单位的币种
      towho.transfer(10)
    }
  }

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3lbMlMlh-1572335194922)(.\img\2.png)]

再来看一段代码:

pragma solidity ^0.4.4;
 
contract Test {
  address public _owner;
  uint public _number;

  function Test() {
    _owner = this;
    _number = 100;
  }
  
  function msgSenderAddress() constant returns (address) {
    return msg.sender;
  }
  
  function setNumberAdd1() {
    _number = _number + 5;
  }
  
  function setNumberAdd2() {
    if (_owner == msg.sender) {
        _number = _number + 10;
    }
  }
  function returnContractAddress() constant returns (address) {
    return this;
  }
} 

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-960C8IXc-1572335194926)(.\img\3.png)]

  • msg.sender就是当前调用方法的用户地址
  • this指的是当前合约的地址
  • address支持各种算数运算符

○ send() 函数
send 与transfer对应,但更底层。如果执行失败,transfer不会因异常停止,而send会返回false。

警告:send() 执行有一些风险:如果调用栈的深度超过1024或gas耗光,交易都会失败。因此,为了保证安全,必须检查send的返回值,如果交易失败,会回退以太币。如果用transfer会更好。

○ call(), callcode() 和 delegatecall() 函数
call函数返回一个bool值,以表明执行成功与否。正常结束返回true,异常终止返回false。但无法获取到结果数据,因为需要提前知道返回的数据的编码和数据大小(因不知道对方使用的协议格式,所以也不会知道返回的结果如何解析)。
还可以提供.gas()修饰器进行调用:

引用类型

数据位置:memory ,storage
一般的定义会有默认的存储位置,但是我们可以通过memory和storage来改变默认的存储地址

memory:
就是我们常说的内存,他不是永久存在的,比如函数的参数和函数的返回值都默认的存在内存中,memory是一个临时的空间,在函数调用的时候出现,在函数调用之后被释放;

storage:
是永久存在的,比如一些复杂变量,状态变量就是存在区块链中,storage对gas的消耗是远远大于memory的;

数组

  • T[k]:元素类型为T,固定长度为k的数组;
  • T[]:元素类型为T,长度为动态的数组;
  • bytes string: 是一种特殊的数组;
  • string 可以转换为bytes,bytes类似于byte[];
    数组的成员有:length和push()
    通过bytes()可以将字符串转换成一个字节数组
pragma solidity ^0.4.4;

contract ArrayTest {
    
  uint[] public u = [1,2,3];
  string s = 'abcde';
  
  function h() public returns (uint) {
    u.push(6);
    return bytes(s).length;
  }
  function f() public view returns (byte) {
    return bytes(s)[1];
  }
  function newM(uint len) constant public returns (uint){
    uint[] memory a = new uint[] (len);
    return a.length;
  }
  function g(uint[3] _data) public constant{
  }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eXGVCyly-1572335194929)(.\img\4.png)]

结构体

pragma solidity ^0.4.4;

contract ArrayTest {
    
   constructor() payable {
       
   }
   
   function testApi () public payable returns (address) {
    //  return msg.sender;(address) 当前用户地址
    //  return msg.value;(uint)
    //  return block.coinbase;(address) 当前矿工的地址
    //  return block.difficulty;(uint) 当前挖矿的难度
    //  return block.number;(uint)当前区块的号码
    //  return block.timestamp;(uint)
    //  return now;(uint)
    //  return tx.gasprice;(uint)
   }
}

solidity的错误处理
如何处理:
assert & require 在老版本中是使用throw来实现抛出错误

  if(msg.sender != owner) { throw; }

  // 现可以完全等价于下面的代码
  if(msg.sender != owner) { revert(); }
  assert(msg.sender == owner);
  require(msg.sender == owner);

注意在 assert()require() 例子中的条件声明,是 if 例子中条件块取反,也就是用 == 代替了 != 。
assert()和require()之间的区别:

  • 可以将 assert() 想象为一个过于自信的实现方式,即使有错误,也会执行并扣除gas。然而 require() 可以被想象为一个更有礼貌些的实现方式,会发现错误,并且原谅所犯错误(译注:不扣除 gas)。
  • 基于以上理解,以上两个函数真正区别在哪里呢?在拜占庭网络更新前, require() 和 assert() 表现完全一样,但是他们的二进制代码却有略微区别---->assert() 使用 0xfe 操作码引起错误条件
    require() 使用 0xfd 操作码引起错误条件
使用assert()的情况:

比如:下标越界,除以零这类情况是assert类型异常;
require是判断外部条件错误的;
检查 overflow/underflow,即:c = a+b; assert(c > b)
检查非变量(invariants),即:assert(this.balance >= totalSupply);
验证改变后的状态,不应该发生的条件
一般地,尽量少使用 assert 调用,如果要使用assert 应该在函数结尾处使用
基本上,require() 应该被用于函数中检查条件,assert() 用于预防不应该发生的情况,但不应该使条件错误。

使用require()的情况:

比如:gas不足,没有匹配到正常的函数的情况下的异常;
assert是判断程序内部逻辑的错误;
验证用户输入,即: require(input<20);
验证外部合约响应,即: require(external.send(amount));
执行合约前,验证状态条件,即: require(block.number > SOME_BLOCK_NUMBER) 或者 require(balance[msg.sender]>=amount)
一般地,尽量使用 require 函数
一般地,require 应该在函数最开始的地方使用
在我们的智能合约最佳实践中有很多使用 require() 的例子供参考。

函数参数

  • 输入参数
  • 输出参数
  • 命名参数
  • 参数解构
pragma solidity ^0.4.20;

contract Test {
    
  function simpleInput(uint a,uint b) public {
    // uint a,uint b 就是输入参数
  }
  function simpleInput2(uint c,uint d) public returns (uint sum) {
    // uint sum 就是输出参数  
  }
  function testSimpleInput() public constant returns (uint sum) {
    // 命名参数是和顺序没有关系的,所以可以调换b和a的顺序 
    sum = simpleInput2({d:1,c:2});
  }
  function test2(uint a,uint b) 
    public constant returns (uint sum,uint mul) {
      // 参数的解构,同时返回两个值
      sum = a + b;
      mul = a * b;
    }
  function test33() public constant returns (uint sum,uint mul) {
     
    (sum,mul) = test2({b:3,a:2});
  }
  function f() public constant returns (uint,bool,uint) {
    return (7,true,2);
  }
  function g() public {

    var (x,y,z) = f();
    (x,z) = (z,x);
  }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FyS58FtK-1572335194933)(.\img\5.png)]

控制语句

控制语句有:if, else, while, do, for, break, continue, return, 三元表达式

没有:switch, goto

pragma solidity ^0.4.20;

contract Test {
  function testWhile() public constant returns(uint) {
    uint i = 0;
    uint sumofOdd = 0;
    
    while(true) {
      i++;
      if( i % 2 == 0) {
          continue;
      }
      if( i > 10 ){
          break;
      }
      sumofOdd += i;
    }
    return sumofOdd;
  }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SEjxd3hr-1572335194937)(.\img\6.png)]

可见性

public 和 private
public:
公开函数是合约接口的一部分,可以通过内部或者消息来进行调用。对于public类型的状态变量,会自动创建一个访问器。 函数默认的可见性是public
private:
私有函数和状态变量仅在当前合约中可以访问,在继承的合约内,不可访问,外部也是无法访问的。
internal:
这种状态变量可供外部和子合约调用,这种类型的函数和private类型的函数一样,智能合约自己内部调用,不过和private不一样的是,他可以在继承的合约里面调用,它和其他语言中的protected不完全一样。
external:
外部函数是合约接口的一部分,只能使用消息调用。

pragma solidity ^0.4.20;

contract Test {
    
  uint public data;
  
  function f(uint a) private returns (uint b) {
    return a + 1;
  }
  function setData(uint a) internal {
    data = a;
  }
  function exSetData(uint a) external {
    data = a;
  }
  function testsetData(uint a) public {
    setData(1);
    this.exSetData(1);
  }
}
contract D {
  function readData() {
    Test test = new Test();
    // test.setData(1); 这样写会报错
    test.exSetData(1);
    test.testsetData(1);
  }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LST2DWTL-1572335194939)(.\img\7.png)]

函数

  • 构造函数
  • 视图函数
  • 纯函数
  • 回退函数
pragma solidity ^0.4.20;
// 构造函数
contract Test {
  uint internal data;
  constructor (uint a) {
    data = a;
  }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vyedhSeu-1572335194941)(.\img\8.png)]

无名函数 也叫回退函数,一个合约里面只能有一个无名函数,这个函数无参数,也无返回值。如果调用合约时,没有匹配上任何一个函数(或者没有传哪怕一点数据),就会调用默认的回退函数。
下述提供给回退函数可执行的操作会比常规的花费得多一点。

  • 写入到存储(storage)
  • 创建一个合约
  • 执行一个外部(external)函数调用,会花费非常多的gas
  • 发送ether
pragma solidity ^0.4.20;

contract Test {
  uint internal data;
  
  constructor (uint a) {
    data = a;
  }
  event EVENTA(uint a);
  
  function testview() public constant returns(uint) {
    // data = 1;
    // emit EVENTA(1);
    return data;
  }
  function f() public pure returns (uint) {
    // this.balance; 会报错
    // msg.value; 会报错
    // return 1 * 2 + data; 会报错
    // 以上会报错是因为在声明f函数的时候,定义的是pure纯函数,所以不对storage进行读写
    return 1 * 2 + 3;
  }
  // 无名函数,必须有payable属性
  function () public payable {
    //  调用无名函数的情况:
    //  1、当我们对合约进行转账的时候会被触发
    //  2、当调用这个合约中没有被定义的函数的时候会调用这个无名函数 
  }
}

实现简单的代币开发

pragma solidity ^0.4.20;

contract Mytoken {

  // 定义一个变量来保存每个地址中的余额
  mapping(address => uint256) public balanceOf;
  // 合约中的构造函数
  constructor(uint256 initSupply) public {
    
    balanceOf[msg.sender] = initSupply;
  }
  function transfer(address _to,uint256 _value) public {

    //  要求账户余额要大于转出的数额 
    require(balanceOf[msg.sender] >=  _value);
    // 目标地址的余额要远远大于转入的值,因为uint定义的是256,如果溢出会出现比原始金额还要小的状态
    require(balanceOf[_to] + _value >= balanceOf[ _to] );
    
    // 转账地址减去要转的值
    balanceOf[msg.sender] -= _value;
    // 目标地址加上转入的金额
    balanceOf[ _to] += _value;
  }
}

实现标准代币接口

但是现实开发中是要求遵循EIPs标准的,EIPs中需要包含以下元素

pragma solidity ^0.4.20;

contract ERC20Interface {
  string public name;
  string public symbol;
  uint8 public decimals;
  uint public totalSupply;

  function balanceOf(address _ower) view returns (uint256 balance);
  // 输入地址就可以获取该地址的代币余额
  function transfer(address _to,uint256 _value) returns (bool success);
  // 调用transfer函数就可以将自己的token地址转给_to 地址,_value 是转账个数
  function transferFrom(address _form,address _to,uint256 _value) returns (bool success);
  // 与approve搭配使用,approve批准之后,调用transferFrom函数来转移token。
  function approve(address _spender,uint _value) returns (bool success);
  // 批准_spender账户从自己的账户转移_value个token。可以分多次转移
  function allowance(address _owner,address _spender) view returns (uint256 remaining);
  // 批准_spender账户从自己的账户转移_value个token。可以分多次转移

  // 事件
  event Transfer(address indexed _from,address indexed _to,uint256 _value);
  // 当成功转移token时,一定要触发Transfer事件
  event Approval(address indexed _owner,address indexed _spender,uint256 _value);
  // 当调用approval函数成功时,一定要触发Approval事件
}

实现标准代币

pragma solidity ^0.4.20;

contract ERC20Interface {
  string public name;
  string public symbol;
  uint8 public decimals;
  uint public totalSupply;

  // function balanceOf(address _ower) view returns (uint256 balance);
  function transfer(address _to,uint256 _value) returns (bool success);
  function transferFrom(address _form,address _to,uint256 _value) returns (bool success);
  function approve(address _spender,uint _value) returns (bool success);
  function allowance(address _owner,address _spender) view returns (uint256 remaining);
  event Transfer(address indexed _from,address indexed _to,uint256 _value);
  event Approval(address indexed _owner,address indexed _spender,uint256 _value);
}

//  approve、transferFrom及allowance解释:
// 账户A有1000个ETH,想允许B账户随意调用100个ETH。A账户按照以下形式调用approve函数approve(B,100)。
// 当B账户想用这100个ETH中的10个ETH给C账户时,则调用transferFrom(A, C, 10)。这时调用allowance(A, B)可以查看B账户还能够调用A账户多少个token。


// contract ERC20 is ERC20Interface 表示合约ERC20继承了合约ERC20Interface
contract ERC20 is ERC20Interface {
  // 用mapping来储存每个地址和他对应的余额
  mapping (address => uint256) public balanceOf;
  mapping (address => mapping (address => uint256)) internal allowed;
  // 表示allowed这个账号可以操控address里面的金额
  constructor() public {
    name = "BTC"; //代币名称叫EmmaToken
    symbol = "IMOOC";   //域名
    decimals = 0;//小数位为0
    totalSupply = 10000;//发行了10000个币
    balanceOf[msg.sender] = totalSupply; //,默认创建者拥有所有的代币
  }
  function balanceOf(address _owner) view returns (uint256 balance){
    // 通过上面mapping的定义,此处可以用这种方法直接返回的传入地址的余额
    return balanceOf[ _owner]
  }

  function transfer(address _to,uint256 _value) returns (bool success) {
    // 在此之前要先做一些余额的检查
    require(_to != address(0)); // _to 这个账号不能为零
    require(balanceOf[msg.sender] >= _value); // 发送者的余额一定要大于转账值
    require(balanceOf[_to] + _value >= balanceOf[_to]);// 表示接受者的余额加上转入值之后也要大于原来的余额,防止溢出的一个判断

    balanceOf[msg.sender] -= _value;
    balanceOf[ _to] += _value;
    emit Transfer(msg.sender,_to,_value);
  }
  
  function transferFrom(address _form,address _to,uint256 _value) returns (bool success) {
    require(_to != address(0)); // _to 这个账号不能为零
    require(balanceOf[_form] >= _value); // 发送者的余额一定要大于转账值
    require(allowed[_form][msg.sender] >= _value);
    require(balanceOf[_to] + _value >= balanceOf[_to]);// 表示接受者的余额加上转入值之后也要大于原来的余额,防止溢出的一个判断

    balanceOf[_form] -= _value;
    balanceOf[ _to] += _value;
    emit Transfer(_form,_to,_value);
    success = true;
  }

  function approve(address _spender,uint _value) returns (bool success) {
    // 表示允许 _spender 账户调用msg.sender的value 个币
    allowed[msg.sender][_spender] = _value;
    // 触发Approval事件
    emit Approval(msg.sender, _spender, _value);

    success = true;
  }

  function allowance(address _owner,address _spender) view returns (uint256 remaining) {
    return allowed[_owner][_spender];
  }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZfIGmPnM-1572335194943)(.\img\9.png)]

搭建项目

  • 了解工具

    ----->truffle
// 安装truffle
npm i -g truffle

// 创建文件夹
mkdir pet-shop
cd pet-shop

// 初始化并下载框架
truffle init
// 可以用ls或者tree命令查看文件结构

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-65DC33md-1572335194945)(.\img\10.png)]
第二种创建方式:

mkdir pet-shop2
cd pet-shop2

// 用unbox来创建
truffle unbox pet-shop

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6usixyui-1572335194946)(.\img\11.png)]

可以看出每次的初始化都会为我们生成几个文件

  • contracts/:智能合约的目录
  • migrations/:用来做部署的目录
  • test/:测试的目录
  • truffle.js: truffle的配置文件

-----> ganache
用来模拟一个本地内存的节点的工具,可以理解为是本地的区块链,这个链只有一个节点,里面会有10个地址账户,每个账户有100的余额,为了方便测试转账

抽象合约

抽象合约和继承相关,类似于上面的例子

pragma solidity ^0.4.0;
 
contract Feline {
    function utterance() public returns (bytes32);
}

这样的合约不能被编译(即使它们同时包含具体函数和抽象函数),但它们可以用作父合约:

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

et-shop

[外链图片转存中...(img-6usixyui-1572335194946)]

可以看出每次的初始化都会为我们生成几个文件
+ `contracts/`:智能合约的目录
+ `migrations/`:用来做部署的目录
+ `test/`:测试的目录
+ `truffle.js`: truffle的配置文件

-----> ganache
用来模拟一个本地内存的节点的工具,可以理解为是本地的区块链,这个链只有一个节点,里面会有10个地址账户,每个账户有100的余额,为了方便测试转账


### 抽象合约
抽象合约和继承相关,类似于上面的例子
```javascript
pragma solidity ^0.4.0;
 
contract Feline {
    function utterance() public returns (bytes32);
}

这样的合约不能被编译(即使它们同时包含具体函数和抽象函数),但它们可以用作父合约:

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

如果一个合约是从抽象合约中继承的,但没实现所有的函数,则它也是抽象合约。

你可能感兴趣的:(JavaScript,DAPP)